/*
 @license textAngular
 Author : Austin Anderson
 License : 2013 MIT
 Version 1.5.0

 See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
 */

/*
 Commonjs package manager support (eg componentjs).
 */

/* istanbul ignore next:  */
'undefined' != typeof module && 'undefined' != typeof exports && module.exports === exports && (module.exports = 'textAngular');

(function () { // encapsulate all variables so they don't become global vars
    "use strict";
// IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
// We need this as IE sometimes plays funny tricks with the contenteditable.
// ----------------------------------------------------------
// If you're not in IE (or IE version is less than 5) then:
// ie === undefined
// If you're in IE (>=5) then you can determine which version:
// ie === 7; // IE7
// Thus, to detect IE:
// if (ie) {}
// And to detect the version:
// ie === 6 // IE6
// ie > 7 // IE8, IE9, IE10 ...
// ie < 9 // Anything less than IE9
// ----------------------------------------------------------
    /* istanbul ignore next: untestable browser check */
    var _browserDetect = {
        ie: (function () {
            var undef,
                v = 3,
                div = document.createElement('div'),
                all = div.getElementsByTagName('i');

            while (
                div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
                    all[0]
                );

            return v > 4 ? v : undef;
        }()),
        webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
    };

// fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
// this is set true when a blur occurs as the blur of the ta-bind triggers before the click
    var globalContentEditableBlur = false;
    /* istanbul ignore next: Browser Un-Focus fix for webkit */
    if (_browserDetect.webkit) {
        document.addEventListener("mousedown", function (_event) {
            var e = _event || window.event;
            var curelement = e.target;
            if (globalContentEditableBlur && curelement !== null) {
                var isEditable = false;
                var tempEl = curelement;
                while (tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable) {
                    isEditable = tempEl.contentEditable === 'true';
                    tempEl = tempEl.parentNode;
                }
                if (!isEditable) {
                    document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
                    curelement.focus(); // focus the wanted element.
                    if (curelement.select) {
                        curelement.select(); // use select to place cursor for input elements.
                    }
                }
            }
            globalContentEditableBlur = false;
        }, false); // add global click handler
        angular.element(document).ready(function () {
            angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" class="ta-hidden-input" aria-hidden="true" unselectable="on" tabIndex="-1">'));
        });
    }

// Gloabl to textAngular REGEXP vars for block and list elements.

    var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i;
    var LISTELEMENTS = /^(ul|li|ol)$/i;
    var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
    /* istanbul ignore next: trim shim for older browsers */
    if (!String.prototype.trim) {
        String.prototype.trim = function () {
            return this.replace(/^\s+|\s+$/g, '');
        };
    }

    /*
     Custom stylesheet for the placeholders rules.
     Credit to: http://davidwalsh.name/add-rules-stylesheets
     */
    var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule, _getRuleIndex;
    /* istanbul ignore else: IE <8 test*/
    if (_browserDetect.ie > 8 || _browserDetect.ie === undefined) {
        var _sheets = document.styleSheets;
        /* istanbul ignore next: preference for stylesheet loaded externally */
        for (var i = 0; i < _sheets.length; i++) {
            if (_sheets[i].media.length === 0 || _sheets[i].media.mediaText.match(/(all|screen)/ig)) {
                if (_sheets[i].href) {
                    if (_sheets[i].href.match(/textangular\.(min\.|)css/ig)) {
                        sheet = _sheets[i];
                        break;
                    }
                }
            }
        }
        /* istanbul ignore next: preference for stylesheet loaded externally */
        if (!sheet) {
            // this sheet is used for the placeholders later on.
            sheet = (function () {
                // Create the <style> tag
                var style = document.createElement("style");
                /* istanbul ignore else : WebKit hack :( */
                if (_browserDetect.webkit) style.appendChild(document.createTextNode(""));

                // Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
                document.getElementsByTagName('head')[0].appendChild(style);

                return style.sheet;
            })();
        }

        // use as: addCSSRule("header", "float: left");
        addCSSRule = function (selector, rules) {
            return _addCSSRule(sheet, selector, rules);
        };
        _addCSSRule = function (_sheet, selector, rules) {
            var insertIndex;
            var insertedRule;
            // This order is important as IE 11 has both cssRules and rules but they have different lengths - cssRules is correct, rules gives an error in IE 11
            /* istanbul ignore next: browser catches */
            if (_sheet.cssRules) insertIndex = Math.max(_sheet.cssRules.length - 1, 0);
            else if (_sheet.rules) insertIndex = Math.max(_sheet.rules.length - 1, 0);

            /* istanbul ignore else: untestable IE option */
            if (_sheet.insertRule) {
                _sheet.insertRule(selector + "{" + rules + "}", insertIndex);
            }
            else {
                _sheet.addRule(selector, rules, insertIndex);
            }
            /* istanbul ignore next: browser catches */
            if (sheet.rules) insertedRule = sheet.rules[insertIndex];
            else if (sheet.cssRules) insertedRule = sheet.cssRules[insertIndex];
            // return the inserted stylesheet rule
            return insertedRule;
        };

        _getRuleIndex = function (rule, rules) {
            var i, ruleIndex;
            for (i = 0; i < rules.length; i++) {
                /* istanbul ignore else: check for correct rule */
                if (rules[i].cssText === rule.cssText) {
                    ruleIndex = i;
                    break;
                }
            }
            return ruleIndex;
        };

        removeCSSRule = function (rule) {
            _removeCSSRule(sheet, rule);
        };
        /* istanbul ignore next: tests are browser specific */
        _removeCSSRule = function (sheet, rule) {
            var rules = sheet.cssRules || sheet.rules;
            if (!rules || rules.length === 0) return;
            var ruleIndex = _getRuleIndex(rule, rules);
            if (sheet.removeRule) {
                sheet.removeRule(ruleIndex);
            } else {
                sheet.deleteRule(ruleIndex);
            }
        };
    }

    angular.module('textAngular.factories', [])
        .factory('taBrowserTag', [function () {
            return function (tag) {
                /* istanbul ignore next: ie specific test */
                if (!tag) return (_browserDetect.ie <= 8) ? 'P' : 'p';
                else if (tag === '') return (_browserDetect.ie === undefined) ? 'div' : (_browserDetect.ie <= 8) ? 'P' : 'p';
                else return (_browserDetect.ie <= 8) ? tag.toUpperCase() : tag;
            };
        }]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function (taCustomRenderers, taDOM) {
        return function (val) {
            var element = angular.element('<div></div>');
            element[0].innerHTML = val;

            angular.forEach(taCustomRenderers, function (renderer) {
                var elements = [];
                // get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
                if (renderer.selector && renderer.selector !== '')
                    elements = element.find(renderer.selector);
                /* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
                else if (renderer.customAttribute && renderer.customAttribute !== '')
                    elements = taDOM.getByAttribute(element, renderer.customAttribute);
                // process elements if any found
                angular.forEach(elements, function (_element) {
                    _element = angular.element(_element);
                    if (renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== '') {
                        if (_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
                    } else renderer.renderLogic(_element);
                });
            });

            return element[0].innerHTML;
        };
    }]).factory('taFixChrome', function () {
        // get whaterever rubbish is inserted in chrome
        // should be passed an html string, returns an html string
        var taFixChrome = function (html) {
            if (!html || !angular.isString(html) || html.length <= 0) return html;
            // grab all elements with a style attibute
            var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
            var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
            while (match = spanMatch.exec(html)) {
                // one of the quoted values ' or "
                /* istanbul ignore next: quotations match */
                styleVal = match[3] || match[4];
                // test for chrome inserted junk
                if (styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)) {
                    // replace original tag with new tag
                    styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
                    newTag = '<' + match[1].trim();
                    if (styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0, 1) + styleVal + match[2].substring(0, 1);
                    newTag += match[5].trim() + ">";
                    finalHtml += html.substring(lastIndex, match.index) + newTag;
                    lastIndex = match.index + match[0].length;
                }
            }
            finalHtml += html.substring(lastIndex);
            // only replace when something has changed, else we get focus problems on inserting lists
            if (lastIndex > 0) {
                // replace all empty strings
                return finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
            } else return html;
        };
        return taFixChrome;
    }).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize) {

        var convert_infos = [
            {
                property: 'font-weight',
                values: ['bold'],
                tag: 'b'
            },
            {
                property: 'font-style',
                values: ['italic'],
                tag: 'i'
            }
        ];

        var styleMatch = [];
        for (var i = 0; i < convert_infos.length; i++) {
            var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
            for (var j = 0; j < convert_infos[i].values.length; j++) {
                /* istanbul ignore next: not needed to be tested yet */
                if (j > 0) _partialStyle += '|';
                _partialStyle += convert_infos[i].values[j];
            }
            _partialStyle += ');)';
            styleMatch.push(_partialStyle);
        }
        var styleRegexString = '(' + styleMatch.join('|') + ')';

        function wrapNested(html, wrapTag) {
            var depth = 0;
            var lastIndex = 0;
            var match;
            var tagRegex = /<[^>]*>/ig;
            while (match = tagRegex.exec(html)) {
                lastIndex = match.index;
                if (match[0].substr(1, 1) === '/') {
                    if (depth === 0) break;
                    else depth--;
                } else depth++;
            }
            return wrapTag +
                html.substring(0, lastIndex) +
                // get the start tags reversed - this is safe as we construct the strings with no content except the tags
                angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
                html.substring(lastIndex);
        }

        function transformLegacyStyles(html) {
            if (!html || !angular.isString(html) || html.length <= 0) return html;
            var i;
            var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
            var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
            while (match = styleElementMatch.exec(html)) {
                // one of the quoted values ' or "
                /* istanbul ignore next: quotations match */
                styleVal = match[3] || match[4];
                var styleRegex = new RegExp(styleRegexString, 'i');
                // test for style values to change
                if (angular.isString(styleVal) && styleRegex.test(styleVal)) {
                    // remove build tag list
                    newTag = '';
                    // init regex here for exec
                    var styleRegexExec = new RegExp(styleRegexString, 'ig');
                    // find relevand tags and build a string of them
                    while (subMatch = styleRegexExec.exec(styleVal)) {
                        for (i = 0; i < convert_infos.length; i++) {
                            if (!!subMatch[(i * 2) + 2]) {
                                newTag += '<' + convert_infos[i].tag + '>';
                            }
                        }
                    }
                    // recursively find more legacy styles in html before this tag and after the previous match (if any)
                    newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
                    // build up html
                    if (lastNewTag.length > 0) {
                        finalHtml += wrapNested(newHtml, lastNewTag);
                    } else finalHtml += newHtml;
                    // grab the style val without the transformed values
                    styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
                    // build the html tag
                    finalHtml += '<' + match[1].trim();
                    if (styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
                    finalHtml += match[5] + '>';
                    // update the start index to after this tag
                    lastIndex = match.index + match[0].length;
                    lastNewTag = newTag;
                }
            }
            if (lastNewTag.length > 0) {
                finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
            }
            else finalHtml += html.substring(lastIndex);
            return finalHtml;
        }

        function transformLegacyAttributes(html) {
            if (!html || !angular.isString(html) || html.length <= 0) return html;
            // replace all align='...' tags with text-align attributes
            var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
            var match, finalHtml = '', lastIndex = 0;
            // match all attr tags
            while (match = attrElementMatch.exec(html)) {
                // add all html before this tag
                finalHtml += html.substring(lastIndex, match.index);
                // record last index after this tag
                lastIndex = match.index + match[0].length;
                // construct tag without the align attribute
                var newTag = '<' + match[1] + match[5];
                // add the style attribute
                if (/style=("([^"]+)"|'([^']+)')/ig.test(newTag)) {
                    /* istanbul ignore next: quotations match */
                    newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
                } else {
                    /* istanbul ignore next: quotations match */
                    newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
                }
                newTag += '>';
                // add to html
                finalHtml += newTag;
            }
            // return with remaining html
            return finalHtml + html.substring(lastIndex);
        }

        return function taSanitize(unsafe, oldsafe, ignore) {
            // unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
            if (!ignore) {
                try {
                    unsafe = transformLegacyStyles(unsafe);
                } catch (e) {
                }
            }

            // unsafe and oldsafe should be valid HTML strings
            // any exceptions (lets say, color for example) should be made here but with great care
            // setup unsafe element for modification
            unsafe = transformLegacyAttributes(unsafe);

            var safe;
            try {
                safe = $sanitize(unsafe);
                // do this afterwards, then the $sanitizer should still throw for bad markup
                if (ignore) safe = unsafe;
            } catch (e) {
                safe = oldsafe || '';
            }

            // Do processing for <pre> tags, removing tabs and return carriages outside of them

            var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
            var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
            var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
            var index = 0;
            var lastIndex = 0;
            var origTag;
            safe = '';
            while ((origTag = re.exec(processedSafe)) !== null && index < _preTags.length) {
                safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
                lastIndex = origTag.index + origTag[0].length;
                index++;
            }
            return safe + processedSafe.substring(lastIndex);
        };
    }]).factory('taToolExecuteAction', ['$q', '$log', function ($q, $log) {
        // this must be called on a toolScope or instance
        return function (editor) {
            if (editor !== undefined) this.$editor = function () {
                return editor;
            };
            var deferred = $q.defer(),
                promise = deferred.promise,
                _editor = this.$editor();
            // pass into the action the deferred function and also the function to reload the current selection if rangy available
            var result;
            try {
                result = this.action(deferred, _editor.startAction());
                // We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
                promise['finally'](function () {
                    _editor.endAction.call(_editor);
                });
            } catch (exc) {
                $log.error(exc);
            }
            if (result || result === undefined) {
                // if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
                deferred.resolve();
            }
        };
    }]);
    angular.module('textAngular.DOM', ['textAngular.factories'])
        .factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function (taSelection, taBrowserTag, $document) {
            var listToDefault = function (listElement, defaultWrap) {
                var $target, i;
                // if all selected then we should remove the list
                // grab all li elements and convert to taDefaultWrap tags
                var children = listElement.find('li');
                for (i = children.length - 1; i >= 0; i--) {
                    $target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
                    listElement.after($target);
                }
                listElement.remove();
                taSelection.setSelectionToElementEnd($target[0]);
            };
            var selectLi = function (liElement) {
                if (/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
                else taSelection.setSelectionToElementEnd(liElement);
            };
            var listToList = function (listElement, newListTag) {
                var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
                listElement.after($target);
                listElement.remove();
                selectLi($target.find('li')[0]);
            };
            var childElementsToList = function (elements, listElement, newListTag) {
                var html = '';
                for (var i = 0; i < elements.length; i++) {
                    html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
                }
                var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
                listElement.after($target);
                listElement.remove();
                selectLi($target.find('li')[0]);
            };
            return function (taDefaultWrap, topNode) {
                taDefaultWrap = taBrowserTag(taDefaultWrap);
                return function (command, showUI, options, defaultTagAttributes) {
                    var i, $target, html, _nodes, next, optionsTagName, selectedElement;
                    var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
                    try {
                        selectedElement = taSelection.getSelectionElement();
                    } catch (e) {
                    }
                    var $selected = angular.element(selectedElement);
                    if (selectedElement !== undefined) {
                        var tagName = selectedElement.tagName.toLowerCase();
                        if (command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist') {
                            var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist') ? 'ol' : 'ul');
                            if (tagName === selfTag) {
                                // if all selected then we should remove the list
                                // grab all li elements and convert to taDefaultWrap tags
                                return listToDefault($selected, taDefaultWrap);
                            } else if (tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1) {
                                // catch for the previous statement if only one li exists
                                return listToDefault($selected.parent(), taDefaultWrap);
                            } else if (tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1) {
                                // catch for the previous statement if only one li exists
                                return listToList($selected.parent(), selfTag);
                            } else if (tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')) {
                                // if it's one of those block elements we have to change the contents
                                // if it's a ol/ul we are changing from one to the other
                                if (tagName === 'ol' || tagName === 'ul') {
                                    return listToList($selected, selfTag);
                                } else {
                                    var childBlockElements = false;
                                    angular.forEach($selected.children(), function (elem) {
                                        if (elem.tagName.match(BLOCKELEMENTS)) {
                                            childBlockElements = true;
                                        }
                                    });
                                    if (childBlockElements) {
                                        return childElementsToList($selected.children(), $selected, selfTag);
                                    } else {
                                        return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
                                    }
                                }
                            } else if (tagName.match(BLOCKELEMENTS)) {
                                // if we get here then all the contents of the ta-bind are selected
                                _nodes = taSelection.getOnlySelectedElements();
                                if (_nodes.length === 0) {
                                    // here is if there is only text in ta-bind ie <div ta-bind>test content</div>
                                    $target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
                                    $selected.html('');
                                    $selected.append($target);
                                } else if (_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')) {
                                    if (_nodes[0].tagName.toLowerCase() === selfTag) {
                                        // remove
                                        return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
                                    } else {
                                        return listToList(angular.element(_nodes[0]), selfTag);
                                    }
                                } else {
                                    html = '';
                                    var $nodes = [];
                                    for (i = 0; i < _nodes.length; i++) {
                                        /* istanbul ignore else: catch for real-world can't make it occur in testing */
                                        if (_nodes[i].nodeType !== 3) {
                                            var $n = angular.element(_nodes[i]);
                                            /* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
                                            if (_nodes[i].tagName.toLowerCase() === 'li') continue;
                                            else if (_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul') {
                                                html += $n[0].innerHTML; // if it's a list, add all it's children
                                            } else if (_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')) {
                                                html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
                                            } else {
                                                html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
                                            }
                                            $nodes.unshift($n);
                                        }
                                    }
                                    $target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
                                    $nodes.pop().replaceWith($target);
                                    angular.forEach($nodes, function ($node) {
                                        $node.remove();
                                    });
                                }
                                taSelection.setSelectionToElementEnd($target[0]);
                                return;
                            }
                        } else if (command.toLowerCase() === 'formatblock') {
                            optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
                            if (optionsTagName.trim() === 'default') {
                                optionsTagName = taDefaultWrap;
                                options = '<' + taDefaultWrap + '>';
                            }
                            if (tagName === 'li') $target = $selected.parent();
                            else $target = $selected;
                            // find the first blockElement
                            while (!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')) {
                                $target = $target.parent();
                                /* istanbul ignore next */
                                tagName = ($target[0].tagName || '').toLowerCase();
                            }
                            if (tagName === optionsTagName) {
                                // $target is wrap element
                                _nodes = $target.children();
                                var hasBlock = false;
                                for (i = 0; i < _nodes.length; i++) {
                                    hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
                                }
                                if (hasBlock) {
                                    $target.after(_nodes);
                                    next = $target.next();
                                    $target.remove();
                                    $target = next;
                                } else {
                                    defaultWrapper.append($target[0].childNodes);
                                    $target.after(defaultWrapper);
                                    $target.remove();
                                    $target = defaultWrapper;
                                }
                            } else if ($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')) {
                                //unwrap logic for parent
                                var blockElement = $target.parent();
                                var contents = blockElement.contents();
                                for (i = 0; i < contents.length; i++) {
                                    /* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
                                    if (blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3) {
                                        defaultWrapper = angular.element('<' + taDefaultWrap + '>');
                                        defaultWrapper[0].innerHTML = contents[i].outerHTML;
                                        contents[i] = defaultWrapper[0];
                                    }
                                    blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
                                }
                                blockElement.remove();
                            } else if (tagName.match(LISTELEMENTS)) {
                                // wrapping a list element
                                $target.wrap(options);
                            } else {
                                // default wrap behaviour
                                _nodes = taSelection.getOnlySelectedElements();
                                if (_nodes.length === 0) _nodes = [$target[0]];
                                // find the parent block element if any of the nodes are inline or text
                                for (i = 0; i < _nodes.length; i++) {
                                    if (_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)) {
                                        while (_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)) {
                                            _nodes[i] = _nodes[i].parentNode;
                                        }
                                    }
                                }
                                if (angular.element(_nodes[0]).hasClass('ta-bind')) {
                                    $target = angular.element(options);
                                    $target[0].innerHTML = _nodes[0].innerHTML;
                                    _nodes[0].innerHTML = $target[0].outerHTML;
                                } else if (optionsTagName === 'blockquote') {
                                    // blockquotes wrap other block elements
                                    html = '';
                                    for (i = 0; i < _nodes.length; i++) {
                                        html += _nodes[i].outerHTML;
                                    }
                                    $target = angular.element(options);
                                    $target[0].innerHTML = html;
                                    _nodes[0].parentNode.insertBefore($target[0], _nodes[0]);
                                    for (i = _nodes.length - 1; i >= 0; i--) {
                                        /* istanbul ignore else:  */
                                        if (_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
                                    }
                                }
                                else {
                                    // regular block elements replace other block elements
                                    for (i = 0; i < _nodes.length; i++) {
                                        $target = angular.element(options);
                                        $target[0].innerHTML = _nodes[i].innerHTML;
                                        _nodes[i].parentNode.insertBefore($target[0], _nodes[i]);
                                        _nodes[i].parentNode.removeChild(_nodes[i]);
                                    }
                                }
                            }
                            taSelection.setSelectionToElementEnd($target[0]);
                            return;
                        } else if (command.toLowerCase() === 'createlink') {
                            var tagBegin = '<a href="' + options + '" target="' +
                                    (defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
                                    '">',
                                tagEnd = '</a>',
                                _selection = taSelection.getSelection();
                            if (_selection.collapsed) {
                                // insert text at selection, then select then just let normal exec-command run
                                taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
                            } else if (rangy.getSelection().getRangeAt(0).canSurroundContents()) {
                                var node = angular.element(tagBegin + tagEnd)[0];
                                rangy.getSelection().getRangeAt(0).surroundContents(node);
                            }
                            return;
                        } else if (command.toLowerCase() === 'inserthtml') {
                            taSelection.insertHtml(options, topNode);
                            return;
                        }
                    }
                    try {
                        $document[0].execCommand(command, showUI, options);
                    } catch (e) {
                    }
                };
            };
        }]).service('taSelection', ['$window', '$document', 'taDOM',
        /* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
        function ($window, $document, taDOM) {
            // need to dereference the document else the calls don't work correctly
            var _document = $document[0];
            var rangy = $window.rangy;
            var brException = function (element, offset) {
                /* check if selection is a BR element at the beginning of a container. If so, get
                 * the parentNode instead.
                 * offset should be zero in this case. Otherwise, return the original
                 * element.
                 */
                if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
                    return {
                        element: element.parentNode,
                        offset: 0
                    };
                } else {
                    return {
                        element: element,
                        offset: offset
                    };
                }
            };
            var api = {
                getSelection: function () {
                    var range = rangy.getSelection().getRangeAt(0);
                    var container = range.commonAncestorContainer;
                    var selection = {
                        start: brException(range.startContainer, range.startOffset),
                        end: brException(range.endContainer, range.endOffset),
                        collapsed: range.collapsed
                    };
                    // Check if the container is a text node and return its parent if so
                    container = container.nodeType === 3 ? container.parentNode : container;
                    if (container.parentNode === selection.start.element ||
                        container.parentNode === selection.end.element) {
                        selection.container = container.parentNode;
                    } else {
                        selection.container = container;
                    }
                    return selection;
                },
                getOnlySelectedElements: function () {
                    var range = rangy.getSelection().getRangeAt(0);
                    var container = range.commonAncestorContainer;
                    // Check if the container is a text node and return its parent if so
                    container = container.nodeType === 3 ? container.parentNode : container;
                    return range.getNodes([1], function (node) {
                        return node.parentNode === container;
                    });
                },
                // Some basic selection functions
                getSelectionElement: function () {
                    return api.getSelection().container;
                },
                setSelection: function (el, start, end) {
                    var range = rangy.createRange();

                    range.setStart(el, start);
                    range.setEnd(el, end);

                    rangy.getSelection().setSingleRange(range);
                },
                setSelectionBeforeElement: function (el) {
                    var range = rangy.createRange();

                    range.selectNode(el);
                    range.collapse(true);

                    rangy.getSelection().setSingleRange(range);
                },
                setSelectionAfterElement: function (el) {
                    var range = rangy.createRange();

                    range.selectNode(el);
                    range.collapse(false);

                    rangy.getSelection().setSingleRange(range);
                },
                setSelectionToElementStart: function (el) {
                    var range = rangy.createRange();

                    range.selectNodeContents(el);
                    range.collapse(true);

                    rangy.getSelection().setSingleRange(range);
                },
                setSelectionToElementEnd: function (el) {
                    var range = rangy.createRange();

                    range.selectNodeContents(el);
                    range.collapse(false);
                    if (el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br') {
                        range.startOffset = range.endOffset = range.startOffset - 1;
                    }
                    rangy.getSelection().setSingleRange(range);
                },
                // from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
                // topNode is the contenteditable normally, all manipulation MUST be inside this.
                insertHtml: function (html, topNode) {
                    var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
                    var element = angular.element("<div>" + html + "</div>");
                    var range = rangy.getSelection().getRangeAt(0);
                    var frag = _document.createDocumentFragment();
                    var children = element[0].childNodes;
                    var isInline = true;

                    if (children.length > 0) {
                        // NOTE!! We need to do the following:
                        // check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
                        // If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
                        nodes = [];
                        for (_childI = 0; _childI < children.length; _childI++) {
                            if (!(
                                    (children[_childI].nodeName.toLowerCase() === 'p' && children[_childI].innerHTML.trim() === '') || // empty p element
                                    (children[_childI].nodeType === 3 && children[_childI].nodeValue.trim() === '') // empty text node
                                )) {
                                isInline = isInline && !BLOCKELEMENTS.test(children[_childI].nodeName);
                                nodes.push(children[_childI]);
                            }
                        }
                        for (var _n = 0; _n < nodes.length; _n++) lastNode = frag.appendChild(nodes[_n]);
                        if (!isInline && range.collapsed && /^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML)) range.selectNode(range.startContainer);
                    } else {
                        isInline = true;
                        // paste text of some sort
                        lastNode = frag = _document.createTextNode(html);
                    }

                    // Other Edge case - selected data spans multiple blocks.
                    if (isInline) {
                        range.deleteContents();
                    } else { // not inline insert
                        if (range.collapsed && range.startContainer !== topNode) {
                            if (range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)) {
                                // this log is to catch when innerHTML is something like `<img ...>`
                                parent = range.startContainer;
                                if (range.startOffset === 1) {
                                    // before single tag
                                    range.setStartAfter(parent);
                                    range.setEndAfter(parent);
                                } else {
                                    // after single tag
                                    range.setStartBefore(parent);
                                    range.setEndBefore(parent);
                                }
                            } else {
                                // split element into 2 and insert block element in middle
                                if (range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode) { // if text node
                                    parent = range.startContainer.parentNode;
                                    secondParent = parent.cloneNode();
                                    // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
                                    taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);

                                    // Escape out of the inline tags like b
                                    while (!VALIDELEMENTS.test(parent.nodeName)) {
                                        angular.element(parent).after(secondParent);
                                        parent = parent.parentNode;
                                        var _lastSecondParent = secondParent;
                                        secondParent = parent.cloneNode();
                                        // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
                                        taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
                                    }
                                } else {
                                    parent = range.startContainer;
                                    secondParent = parent.cloneNode();
                                    taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
                                }

                                angular.element(parent).after(secondParent);
                                // put cursor to end of inserted content
                                range.setStartAfter(parent);
                                range.setEndAfter(parent);

                                if (/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())) {
                                    range.setStartBefore(parent);
                                    range.setEndBefore(parent);
                                    angular.element(parent).remove();
                                }
                                if (/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
                                if (parent.nodeName.toLowerCase() === 'li') {
                                    _tempFrag = _document.createDocumentFragment();
                                    for (i = 0; i < frag.childNodes.length; i++) {
                                        element = angular.element('<li>');
                                        taDOM.transferChildNodes(frag.childNodes[i], element[0]);
                                        taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
                                        _tempFrag.appendChild(element[0]);
                                    }
                                    frag = _tempFrag;
                                    if (lastNode) {
                                        lastNode = frag.childNodes[frag.childNodes.length - 1];
                                        lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
                                    }
                                }
                            }
                        } else {
                            range.deleteContents();
                        }
                    }

                    range.insertNode(frag);
                    if (lastNode) {
                        api.setSelectionToElementEnd(lastNode);
                    }
                }
            };
            return api;
        }]).service('taDOM', function () {
        var taDOM = {
            // recursive function that returns an array of angular.elements that have the passed attribute set on them
            getByAttribute: function (element, attribute) {
                var resultingElements = [];
                var childNodes = element.children();
                if (childNodes.length) {
                    angular.forEach(childNodes, function (child) {
                        resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
                    });
                }
                if (element.attr(attribute) !== undefined) resultingElements.push(element);
                return resultingElements;
            },

            transferChildNodes: function (source, target) {
                // clear out target
                target.innerHTML = '';
                while (source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
                return target;
            },

            splitNodes: function (nodes, target1, target2, splitNode, subSplitIndex, splitIndex) {
                if (!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
                var startNodes = document.createDocumentFragment();
                var endNodes = document.createDocumentFragment();
                var index = 0;

                while (nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode) {
                    startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
                    index++;
                }

                if (!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]) {
                    startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
                    nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
                }
                while (nodes.length > 0) endNodes.appendChild(nodes[0]);

                taDOM.transferChildNodes(startNodes, target1);
                taDOM.transferChildNodes(endNodes, target2);
            },

            transferNodeAttributes: function (source, target) {
                for (var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
                return target;
            }
        };
        return taDOM;
    });
    angular.module('textAngular.validators', [])
        .directive('taMaxText', function () {
            return {
                restrict: 'A',
                require: 'ngModel',
                link: function (scope, elem, attrs, ctrl) {
                    var max = parseInt(scope.$eval(attrs.taMaxText));
                    if (isNaN(max)) {
                        throw('Max text must be an integer');
                    }
                    attrs.$observe('taMaxText', function (value) {
                        max = parseInt(value);
                        if (isNaN(max)) {
                            throw('Max text must be an integer');
                        }
                        if (ctrl.$dirty) {
                            ctrl.$validate();
                        }
                    });
                    ctrl.$validators.taMaxText = function (viewValue) {
                        var source = angular.element('<div/>');
                        source.html(viewValue);
                        return source.text().length <= max;
                    };
                }
            };
        }).directive('taMinText', function () {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, elem, attrs, ctrl) {
                var min = parseInt(scope.$eval(attrs.taMinText));
                if (isNaN(min)) {
                    throw('Min text must be an integer');
                }
                attrs.$observe('taMinText', function (value) {
                    min = parseInt(value);
                    if (isNaN(min)) {
                        throw('Min text must be an integer');
                    }
                    if (ctrl.$dirty) {
                        ctrl.$validate();
                    }
                });
                ctrl.$validators.taMinText = function (viewValue) {
                    var source = angular.element('<div/>');
                    source.html(viewValue);
                    return !source.text().length || source.text().length >= min;
                };
            }
        };
    });
    angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
        .service('_taBlankTest', [function () {
            var INLINETAGS_NONBLANK = /<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;
            return function (_defaultTest) {
                return function (_blankVal) {
                    if (!_blankVal) return true;
                    // find first non-tag match - ie start of string or after tag that is not whitespace
                    var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
                    var _firstTagIndex;
                    if (!_firstMatch) {
                        // find the end of the first tag removing all the
                        // Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
                        _blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
                        _firstTagIndex = _blankVal.indexOf('>');
                    } else {
                        _firstTagIndex = _firstMatch.index;
                    }
                    _blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
                    // check for no tags entry
                    if (/^[^<>]+$/i.test(_blankVal)) return false;
                    // this regex is to match any number of whitespace only between two tags
                    if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s|&nbsp;)*<\/[^>]+>$/ig.test(_blankVal)) return true;
                    // this regex tests if there is a tag followed by some optional whitespace and some text after that
                    else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
                    else return true;
                };
            };
        }])
        .directive('taButton', [function () {
            return {
                link: function (scope, element, attrs) {
                    element.attr('unselectable', 'on');
                    element.on('mousedown', function (e, eventData) {
                        /* istanbul ignore else: this is for catching the jqLite testing*/
                        if (eventData) angular.extend(e, eventData);
                        // this prevents focusout from firing on the editor when clicking toolbar buttons
                        e.preventDefault();
                        return false;
                    });
                }
            };
        }])
        .directive('taBind', [
            'taSanitize', '$timeout', '$window', '$document', 'taFixChrome', 'taBrowserTag',
            'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
            '_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
            function (taSanitize, $timeout, $window, $document, taFixChrome, taBrowserTag,
                      taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
                      _taBlankTest, $parse, taDOM, textAngularManager) {
                // Uses for this are textarea or input with ng-model and ta-bind='text'
                // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
                return {
                    priority: 2, // So we override validators correctly
                    require: ['ngModel', '?ngModelOptions'],
                    link: function (scope, element, attrs, controller) {
                        var ngModel = controller[0];
                        var ngModelOptions = controller[1] || {};
                        // the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
                        var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
                        var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
                        var _isReadonly = false;
                        var _focussed = false;
                        var _skipRender = false;
                        var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
                        var _lastKey;
                        // see http://www.javascripter.net/faq/keycodes.htm for good information
                        // NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
                        // BLOCKED_KEYS are special keys...
                        // Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
                        // Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
                        // f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
                        // NumLock, ScrollLock
                        var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i;
                        // UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
                        // Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
                        // Numpad +, Numpad -, (; :), (= +),
                        // (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
                        // NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
                        var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i;
                        var _pasteHandler;

                        // defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
                        // non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
                        var _defaultVal, _defaultTest;

                        var _CTRL_KEY = 0x0001;
                        var _META_KEY = 0x0002;
                        var _ALT_KEY = 0x0004;
                        var _SHIFT_KEY = 0x0008;
                        // map events to special keys...
                        // mappings is an array of maps from events to specialKeys as declared in textAngularSetup
                        var _keyMappings = [
                            //		ctrl/command + z
                            {
                                specialKey: 'UndoKey',
                                forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
                                mustHaveModifiers: [_META_KEY + _CTRL_KEY],
                                keyCode: 90
                            },
                            //		ctrl/command + shift + z
                            {
                                specialKey: 'RedoKey',
                                forbiddenModifiers: _ALT_KEY,
                                mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
                                keyCode: 90
                            },
                            //		ctrl/command + y
                            {
                                specialKey: 'RedoKey',
                                forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
                                mustHaveModifiers: [_META_KEY + _CTRL_KEY],
                                keyCode: 89
                            },
                            //		TabKey
                            {
                                specialKey: 'TabKey',
                                forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
                                mustHaveModifiers: [],
                                keyCode: 9
                            },
                            //		shift + TabKey
                            {
                                specialKey: 'ShiftTabKey',
                                forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
                                mustHaveModifiers: [_SHIFT_KEY],
                                keyCode: 9
                            }
                        ];

                        function _mapKeys(event) {
                            var specialKey;
                            _keyMappings.forEach(function (map) {
                                if (map.keyCode === event.keyCode) {
                                    var netModifiers = (event.metaKey ? _META_KEY : 0) +
                                        (event.ctrlKey ? _CTRL_KEY : 0) +
                                        (event.shiftKey ? _SHIFT_KEY : 0) +
                                        (event.altKey ? _ALT_KEY : 0);
                                    if (map.forbiddenModifiers & netModifiers) return;
                                    if (map.mustHaveModifiers.every(function (modifier) {
                                            return netModifiers & modifier;
                                        })) {
                                        specialKey = map.specialKey;
                                    }
                                }
                            });
                            return specialKey;
                        }

                        // set the default to be a paragraph value
                        if (attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
                        /* istanbul ignore next: ie specific test */
                        if (attrs.taDefaultWrap === '') {
                            _defaultVal = '';
                            _defaultTest = (_browserDetect.ie === undefined) ? '<div><br></div>' : (_browserDetect.ie >= 11) ? '<p><br></p>' : (_browserDetect.ie <= 8) ? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
                        } else {
                            _defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11) ?
                            '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
                                (_browserDetect.ie <= 8) ?
                                '<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
                                '<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
                            _defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11) ?
                            '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
                                (_browserDetect.ie <= 8) ?
                                '<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
                                '<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
                        }

                        /* istanbul ignore else */
                        if (!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support

                        var _blankTest = _taBlankTest(_defaultTest);

                        var _ensureContentWrapped = function (value) {
                            if (_blankTest(value)) return value;
                            var domTest = angular.element("<div>" + value + "</div>");
                            //console.log('domTest.children().length():', domTest.children().length);
                            if (domTest.children().length === 0) {
                                value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
                            } else {
                                var _children = domTest[0].childNodes;
                                var i;
                                var _foundBlockElement = false;
                                for (i = 0; i < _children.length; i++) {
                                    if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
                                }
                                if (!_foundBlockElement) {
                                    value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
                                }
                                else {
                                    value = "";
                                    for (i = 0; i < _children.length; i++) {
                                        var node = _children[i];
                                        var nodeName = node.nodeName.toLowerCase();
                                        //console.log(nodeName);
                                        if (nodeName === '#comment') {
                                            value += '<!--' + node.nodeValue + '-->';
                                        } else if (nodeName === '#text') {
                                            // determine if this is all whitespace, if so, we will leave it as it is.
                                            // otherwise, we will wrap it as it is
                                            var text = node.textContent;
                                            if (!text.trim()) {
                                                // just whitespace
                                                value += text;
                                            } else {
                                                // not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
                                                value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
                                            }
                                        } else if (!nodeName.match(BLOCKELEMENTS)) {
                                            /* istanbul ignore  next: Doesn't seem to trigger on tests */
                                            var _subVal = (node.outerHTML || node.nodeValue);
                                            /* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
                                            if (_subVal.trim() !== '')
                                                value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
                                            else value += _subVal;
                                        } else {
                                            value += node.outerHTML;
                                        }
                                    }
                                }
                            }
                            //console.log(value);
                            return value;
                        };

                        if (attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);

                        element.addClass('ta-bind');

                        var _undoKeyupTimeout;

                        scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
                            _stack: [],
                            _index: 0,
                            _max: 1000,
                            push: function (value) {
                                if ((typeof value === "undefined" || value === null) ||
                                    ((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
                                if (this._index < this._stack.length - 1) {
                                    this._stack = this._stack.slice(0, this._index + 1);
                                }
                                this._stack.push(value);
                                if (_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
                                if (this._stack.length > this._max) this._stack.shift();
                                this._index = this._stack.length - 1;
                                return value;
                            },
                            undo: function () {
                                return this.setToIndex(this._index - 1);
                            },
                            redo: function () {
                                return this.setToIndex(this._index + 1);
                            },
                            setToIndex: function (index) {
                                if (index < 0 || index > this._stack.length - 1) {
                                    return undefined;
                                }
                                this._index = index;
                                return this.current();
                            },
                            current: function () {
                                return this._stack[this._index];
                            }
                        };

                        var _redoUndoTimeout;
                        var _undo = scope['$undoTaBind' + (attrs.id || '')] = function () {
                            /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
                            if (!_isReadonly && _isContentEditable) {
                                var content = ngModel.$undoManager.undo();
                                if (typeof content !== "undefined" && content !== null) {
                                    _setInnerHTML(content);
                                    _setViewValue(content, false);
                                    if (_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
                                    _redoUndoTimeout = $timeout(function () {
                                        element[0].focus();
                                        taSelection.setSelectionToElementEnd(element[0]);
                                    }, 1);
                                }
                            }
                        };

                        var _redo = scope['$redoTaBind' + (attrs.id || '')] = function () {
                            /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
                            if (!_isReadonly && _isContentEditable) {
                                var content = ngModel.$undoManager.redo();
                                if (typeof content !== "undefined" && content !== null) {
                                    _setInnerHTML(content);
                                    _setViewValue(content, false);
                                    /* istanbul ignore next */
                                    if (_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
                                    _redoUndoTimeout = $timeout(function () {
                                        element[0].focus();
                                        taSelection.setSelectionToElementEnd(element[0]);
                                    }, 1);
                                }
                            }
                        };

                        // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
                        var _compileHtml = function () {
                            if (_isContentEditable) return element[0].innerHTML;
                            if (_isInputFriendly) return element.val();
                            throw ('textAngular Error: attempting to update non-editable taBind');
                        };

                        var _setViewValue = function (_val, triggerUndo, skipRender) {
                            _skipRender = skipRender || false;
                            if (typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
                            if (typeof _val === "undefined" || _val === null) _val = _compileHtml();
                            if (_blankTest(_val)) {
                                // this avoids us from tripping the ng-pristine flag if we click in and out with out typing
                                if (ngModel.$viewValue !== '') ngModel.$setViewValue('');
                                if (triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
                            } else {
                                _reApplyOnSelectorHandlers();
                                if (ngModel.$viewValue !== _val) {
                                    ngModel.$setViewValue(_val);
                                    if (triggerUndo) ngModel.$undoManager.push(_val);
                                }
                            }
                            ngModel.$render();
                        };

                        //used for updating when inserting wrapped elements
                        scope['updateTaBind' + (attrs.id || '')] = function () {
                            if (!_isReadonly) _setViewValue(undefined, undefined, true);
                        };

                        // catch DOM XSS via taSanitize
                        // Sanitizing both ways is identical
                        var _sanitize = function (unsafe) {
                            return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
                        };

                        // trigger the validation calls
                        if (element.attr('required')) ngModel.$validators.required = function (modelValue, viewValue) {
                            return !_blankTest(modelValue || viewValue);
                        };
                        // parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
                        ngModel.$parsers.push(_sanitize);
                        ngModel.$parsers.unshift(_ensureContentWrapped);
                        // because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
                        ngModel.$formatters.push(_sanitize);
                        ngModel.$formatters.unshift(_ensureContentWrapped);
                        ngModel.$formatters.unshift(function (value) {
                            return ngModel.$undoManager.push(value || '');
                        });

                        //this code is used to update the models when data is entered/deleted
                        if (_isInputFriendly) {
                            scope.events = {};
                            if (!_isContentEditable) {
                                // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
                                element.on('change blur', scope.events.change = scope.events.blur = function () {
                                    if (!_isReadonly) ngModel.$setViewValue(_compileHtml());
                                });

                                element.on('keydown', scope.events.keydown = function (event, eventData) {
                                    /* istanbul ignore else: this is for catching the jqLite testing*/
                                    if (eventData) angular.extend(event, eventData);
                                    // Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
                                    /* istanbul ignore else: otherwise normal functionality */
                                    if (event.keyCode === 9) { // tab was pressed
                                        // get caret position/selection
                                        var start = this.selectionStart;
                                        var end = this.selectionEnd;

                                        var value = element.val();
                                        if (event.shiftKey) {
                                            // find \t
                                            var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
                                            if (_tab !== -1 && _tab >= _linebreak) {
                                                // set textarea value to: text before caret + tab + text after caret
                                                element.val(value.substring(0, _tab) + value.substring(_tab + 1));

                                                // put caret at right position again (add one for the tab)
                                                this.selectionStart = this.selectionEnd = start - 1;
                                            }
                                        } else {
                                            // set textarea value to: text before caret + tab + text after caret
                                            element.val(value.substring(0, start) + "\t" + value.substring(end));

                                            // put caret at right position again (add one for the tab)
                                            this.selectionStart = this.selectionEnd = start + 1;
                                        }
                                        // prevent the focus lose
                                        event.preventDefault();
                                    }
                                });

                                var _repeat = function (string, n) {
                                    var result = '';
                                    for (var _n = 0; _n < n; _n++) result += string;
                                    return result;
                                };

                                // add a forEach function that will work on a NodeList, etc..
                                var forEach = function (array, callback, scope) {
                                    for (var i = 0; i < array.length; i++) {
                                        callback.call(scope, i, array[i]);
                                    }
                                };

                                // handle <ul> or <ol> nodes
                                var recursiveListFormat = function (listNode, tablevel) {
                                    var _html = '';
                                    var _subnodes = listNode.childNodes;
                                    tablevel++;
                                    // tab out and add the <ul> or <ol> html piece
                                    _html += _repeat('\t', tablevel - 1) + listNode.outerHTML.substring(0, 4);
                                    forEach(_subnodes, function (index, node) {
                                        /* istanbul ignore next: browser catch */
                                        var nodeName = node.nodeName.toLowerCase();
                                        if (nodeName === '#comment') {
                                            _html += '<!--' + node.nodeValue + '-->';
                                            return;
                                        }
                                        if (nodeName === '#text') {
                                            _html += node.textContent;
                                            return;
                                        }
                                        /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
                                        if (!node.outerHTML) {
                                            // no html to add
                                            return;
                                        }
                                        if (nodeName === 'ul' || nodeName === 'ol') {
                                            _html += '\n' + recursiveListFormat(node, tablevel);
                                        }
                                        else {
                                            // no reformatting within this subnode, so just do the tabing...
                                            _html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
                                        }
                                    });
                                    // now add on the </ol> or </ul> piece
                                    _html += '\n' + _repeat('\t', tablevel - 1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
                                    return _html;
                                };
                                // handle formating of something like:
                                // <ol><!--First comment-->
                                //  <li>Test Line 1<!--comment test list 1--></li>
                                //    <ul><!--comment ul-->
                                //      <li>Nested Line 1</li>
                                //        <!--comment between nested lines--><li>Nested Line 2</li>
                                //    </ul>
                                //  <li>Test Line 3</li>
                                // </ol>
                                ngModel.$formatters.unshift(function (htmlValue) {
                                    // tabulate the HTML so it looks nicer
                                    //
                                    // first get a list of the nodes...
                                    // we do this by using the element parser...
                                    //
                                    // doing this -- which is simpiler -- breaks our tests...
                                    //var _nodes=angular.element(htmlValue);
                                    var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
                                    if (_nodes.length > 0) {
                                        // do the reformatting of the layout...
                                        htmlValue = '';
                                        forEach(_nodes, function (index, node) {
                                            var nodeName = node.nodeName.toLowerCase();
                                            if (nodeName === '#comment') {
                                                htmlValue += '<!--' + node.nodeValue + '-->';
                                                return;
                                            }
                                            if (nodeName === '#text') {
                                                htmlValue += node.textContent;
                                                return;
                                            }
                                            /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
                                            if (!node.outerHTML) {
                                                // nothing to format!
                                                return;
                                            }
                                            if (htmlValue.length > 0) {
                                                // we aready have some content, so drop to a new line
                                                htmlValue += '\n';
                                            }
                                            if (nodeName === 'ul' || nodeName === 'ol') {
                                                // okay a set of list stuff we want to reformat in a nested way
                                                htmlValue += '' + recursiveListFormat(node, 0);
                                            }
                                            else {
                                                // just use the original without any additional formating
                                                htmlValue += '' + node.outerHTML;
                                            }
                                        });
                                    }
                                    return htmlValue;
                                });
                            } else {
                                // all the code specific to contenteditable divs
                                var _processingPaste = false;
                                /* istanbul ignore next: phantom js cannot test this for some reason */
                                var processpaste = function (text) {
                                    /* istanbul ignore else: don't care if nothing pasted */
                                    if (text && text.trim().length) {
                                        // test paste from word/microsoft product
                                        if (text.match(/class=["']*Mso(Normal|List)/i) || text.match(/content=["']*Word.Document/i)) {
                                            var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
                                            if (!textFragment) textFragment = text;
                                            else textFragment = textFragment[1];
                                            textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
                                            var dom = angular.element("<div>" + textFragment + "</div>");
                                            var targetDom = angular.element("<div></div>");
                                            var _list = {
                                                element: null,
                                                lastIndent: [],
                                                lastLi: null,
                                                isUl: false
                                            };
                                            _list.lastIndent.peek = function () {
                                                var n = this.length;
                                                if (n > 0) return this[n - 1];
                                            };
                                            var _resetList = function (isUl) {
                                                _list.isUl = isUl;
                                                _list.element = angular.element(isUl ? "<ul>" : "<ol>");
                                                _list.lastIndent = [];
                                                _list.lastIndent.peek = function () {
                                                    var n = this.length;
                                                    if (n > 0) return this[n - 1];
                                                };
                                                _list.lastLevelMatch = null;
                                            };
                                            for (var i = 0; i <= dom[0].childNodes.length; i++) {
                                                if (!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text") {
                                                    continue;
                                                } else {
                                                    var tagName = dom[0].childNodes[i].tagName.toLowerCase();
                                                    if (tagName !== "p" && tagName !== "h1" && tagName !== "h2" && tagName !== "h3" && tagName !== "h4" && tagName !== "h5" && tagName !== "h6") {
                                                        continue;
                                                    }
                                                }
                                                var el = angular.element(dom[0].childNodes[i]);
                                                var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);

                                                if (_listMatch) {
                                                    if (el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1) {
                                                        continue;
                                                    }
                                                    var isUl = _listMatch[1].toLowerCase() === "bullet" || (_listMatch[1].toLowerCase() !== "number" && !(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].innerHTML) || /^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].childNodes[0].innerHTML)));
                                                    var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
                                                    var indent = parseFloat((_indentMatch) ? _indentMatch[1] : 0);
                                                    var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
                                                    // prefers the mso-list syntax

                                                    if (_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);

                                                    if ((_levelMatch && (!_list.lastLevelMatch || _levelMatch[1] !== _list.lastLevelMatch[1])) || !_listMatch[3] || _listMatch[3].toLowerCase() === "first" || (_list.lastIndent.peek() === null) || (_list.isUl !== isUl && _list.lastIndent.peek() === indent)) {
                                                        _resetList(isUl);
                                                        targetDom.append(_list.element);
                                                    } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent) {
                                                        _list.element = angular.element(isUl ? "<ul>" : "<ol>");
                                                        _list.lastLi.append(_list.element);
                                                    } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent) {
                                                        while (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent) {
                                                            if (_list.element.parent()[0].tagName.toLowerCase() === 'li') {
                                                                _list.element = _list.element.parent();
                                                                continue;
                                                            } else if (/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())) {
                                                                _list.element = _list.element.parent();
                                                            } else { // else it's it should be a sibling
                                                                break;
                                                            }
                                                            _list.lastIndent.pop();
                                                        }
                                                        _list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
                                                        if (isUl !== _list.isUl) {
                                                            _resetList(isUl);
                                                            targetDom.append(_list.element);
                                                        }
                                                    }

                                                    _list.lastLevelMatch = _levelMatch;
                                                    if (indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
                                                    _list.lastLi = angular.element("<li>");
                                                    _list.element.append(_list.lastLi);
                                                    _list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
                                                    el.remove();
                                                } else {
                                                    _resetList(false);
                                                    targetDom.append(el);
                                                }
                                            }
                                            var _unwrapElement = function (node) {
                                                node = angular.element(node);
                                                for (var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
                                                node.remove();
                                            };

                                            angular.forEach(targetDom.find('span'), function (node) {
                                                node.removeAttribute('lang');
                                                if (node.attributes.length <= 0) _unwrapElement(node);
                                            });
                                            angular.forEach(targetDom.find('font'), _unwrapElement);
                                            text = targetDom.html();
                                        } else {
                                            // remove unnecessary chrome insert
                                            text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
                                            if (text.match(/<[^>]*?(ta-bind)[^>]*?>/)) {
                                                // entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
                                                if (text.match(/<[^>]*?(text-angular)[^>]*?>/)) {
                                                    var _el = angular.element("<div>" + text + "</div>");
                                                    _el.find('textarea').remove();
                                                    var binds = taDOM.getByAttribute(_el, 'ta-bind');
                                                    for (var _b = 0; _b < binds.length; _b++) {
                                                        var _target = binds[_b][0].parentNode.parentNode;
                                                        for (var _c = 0; _c < binds[_b][0].childNodes.length; _c++) {
                                                            _target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
                                                        }
                                                        _target.parentNode.removeChild(_target);
                                                    }
                                                    text = _el.html().replace('<br class="Apple-interchange-newline">', '');
                                                }
                                            } else if (text.match(/^<span/)) {
                                                // in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
                                                // if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
                                                // on paste from even ourselves!
                                                if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) {
                                                    text = text.replace(/<(|\/)span[^>]*?>/ig, '');
                                                }
                                            }
                                            // Webkit on Apple tags
                                            text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;');
                                        }

                                        if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
                                            // insert missing parent of li element
                                            text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
                                        }

                                        // parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
                                        text = text.replace(/^[ |\u00A0]+/gm, function (match) {
                                            var result = '';
                                            for (var i = 0; i < match.length; i++) {
                                                result += '&nbsp;';
                                            }
                                            return result;
                                        }).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');

                                        if (_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;

                                        text = taSanitize(text, '', _disableSanitizer);

                                        taSelection.insertHtml(text, element[0]);
                                        $timeout(function () {
                                            ngModel.$setViewValue(_compileHtml());
                                            _processingPaste = false;
                                            element.removeClass('processing-paste');
                                        }, 0);
                                    } else {
                                        _processingPaste = false;
                                        element.removeClass('processing-paste');
                                    }
                                };

                                element.on('paste', scope.events.paste = function (e, eventData) {
                                    /* istanbul ignore else: this is for catching the jqLite testing*/
                                    if (eventData) angular.extend(e, eventData);
                                    if (_isReadonly || _processingPaste) {
                                        e.stopPropagation();
                                        e.preventDefault();
                                        return false;
                                    }

                                    // Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
                                    _processingPaste = true;
                                    element.addClass('processing-paste');
                                    var pastedContent;
                                    var clipboardData = (e.originalEvent || e).clipboardData;
                                    if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
                                        var _types = "";
                                        for (var _t = 0; _t < clipboardData.types.length; _t++) {
                                            _types += " " + clipboardData.types[_t];
                                        }
                                        /* istanbul ignore next: browser tests */
                                        if (/text\/html/i.test(_types)) {
                                            pastedContent = clipboardData.getData('text/html');
                                        } else if (/text\/plain/i.test(_types)) {
                                            pastedContent = clipboardData.getData('text/plain');
                                        }

                                        processpaste(pastedContent);
                                        e.stopPropagation();
                                        e.preventDefault();
                                        return false;
                                    } else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
                                        var _savedSelection = $window.rangy.saveSelection(),
                                            _tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
                                        $document.find('body').append(_tempDiv);
                                        _tempDiv[0].focus();
                                        $timeout(function () {
                                            // restore selection
                                            $window.rangy.restoreSelection(_savedSelection);
                                            processpaste(_tempDiv[0].innerHTML);
                                            element[0].focus();
                                            _tempDiv.remove();
                                        }, 0);
                                    }
                                });
                                element.on('cut', scope.events.cut = function (e) {
                                    // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
                                    if (!_isReadonly) $timeout(function () {
                                        ngModel.$setViewValue(_compileHtml());
                                    }, 0);
                                    else e.preventDefault();
                                });

                                element.on('keydown', scope.events.keydown = function (event, eventData) {
                                    /* istanbul ignore else: this is for catching the jqLite testing*/
                                    if (eventData) angular.extend(event, eventData);
                                    event.specialKey = _mapKeys(event);
                                    var userSpecialKey;
                                    /* istanbul ignore next: difficult to test */
                                    taOptions.keyMappings.forEach(function (mapping) {
                                        if (event.specialKey === mapping.commandKeyCode) {
                                            // taOptions has remapped this binding... so
                                            // we disable our own
                                            event.specialKey = undefined;
                                        }
                                        if (mapping.testForKey(event)) {
                                            userSpecialKey = mapping.commandKeyCode;
                                        }
                                        if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
                                            // this is necessary to fully stop the propagation.
                                            if (!mapping.enablePropagation) {
                                                event.preventDefault();
                                            }
                                        }
                                    });
                                    /* istanbul ignore next: difficult to test */
                                    if (typeof userSpecialKey !== 'undefined') {
                                        event.specialKey = userSpecialKey;
                                    }
                                    /* istanbul ignore next: difficult to test as can't seem to select */
                                    if ((typeof event.specialKey !== 'undefined') && (
                                            event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
                                        )) {
                                        event.preventDefault();
                                        textAngularManager.sendKeyCommand(scope, event);
                                    }
                                    /* istanbul ignore else: readonly check */
                                    if (!_isReadonly) {
                                        if (event.specialKey === 'UndoKey') {
                                            _undo();
                                            event.preventDefault();
                                        }
                                        if (event.specialKey === 'RedoKey') {
                                            _redo();
                                            event.preventDefault();
                                        }
                                        /* istanbul ignore next: difficult to test as can't seem to select */
                                        if (event.keyCode === 13 && !event.shiftKey) {
                                            var contains = function (a, obj) {
                                                for (var i = 0; i < a.length; i++) {
                                                    if (a[i] === obj) {
                                                        return true;
                                                    }
                                                }
                                                return false;
                                            };
                                            var $selection;
                                            var selection = taSelection.getSelectionElement();
                                            if (!selection.tagName.match(VALIDELEMENTS)) return;
                                            var _new = angular.element(_defaultVal);
                                            // if we are in the last element of a blockquote, or ul or ol and the element is blank
                                            // we need to pull the element outside of the said type
                                            var moveOutsideElements = ['blockquote', 'ul', 'ol'];
                                            if (contains(moveOutsideElements, selection.parentNode.tagName.toLowerCase())) {
                                                if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && !selection.nextSibling) {
                                                    // if last element is blank, pull element outside.
                                                    $selection = angular.element(selection);
                                                    var _parent = $selection.parent();
                                                    _parent.after(_new);
                                                    $selection.remove();
                                                    if (_parent.children().length === 0) _parent.remove();
                                                    taSelection.setSelectionToElementStart(_new[0]);
                                                    event.preventDefault();
                                                }
                                                if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim())) {
                                                    $selection = angular.element(selection);
                                                    $selection.after(_new);
                                                    $selection.remove();
                                                    taSelection.setSelectionToElementStart(_new[0]);
                                                    event.preventDefault();
                                                }
                                            }
                                        }
                                    }
                                });
                                var _keyupTimeout;
                                element.on('keyup', scope.events.keyup = function (event, eventData) {
                                    /* istanbul ignore else: this is for catching the jqLite testing*/
                                    if (eventData) angular.extend(event, eventData);
                                    /* istanbul ignore next: FF specific bug fix */
                                    if (event.keyCode === 9) {
                                        var _selection = taSelection.getSelection();
                                        if (_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
                                        return;
                                    }
                                    if (_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
                                    if (!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)) {
                                        // if enter - insert new taDefaultWrap, if shift+enter insert <br/>
                                        if (_defaultVal !== '' && event.keyCode === 13) {
                                            if (!event.shiftKey) {
                                                // new paragraph, br should be caught correctly
                                                var selection = taSelection.getSelectionElement();
                                                while (!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]) {
                                                    selection = selection.parentNode;
                                                }

                                                if (selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')) {
                                                    var _new = angular.element(_defaultVal);
                                                    angular.element(selection).replaceWith(_new);
                                                    taSelection.setSelectionToElementStart(_new[0]);
                                                }
                                            }
                                        }
                                        var val = _compileHtml();
                                        if (_defaultVal !== '' && val.trim() === '') {
                                            _setInnerHTML(_defaultVal);
                                            taSelection.setSelectionToElementStart(element.children()[0]);
                                        } else if (val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== '') {
                                            /* we no longer do this, since there can be comments here and white space
                                             var _savedSelection = $window.rangy.saveSelection();
                                             val = _compileHtml();
                                             val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
                                             _setInnerHTML(val);
                                             $window.rangy.restoreSelection(_savedSelection);
                                             */
                                        }
                                        var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
                                        if (_keyupTimeout) $timeout.cancel(_keyupTimeout);
                                        _keyupTimeout = $timeout(function () {
                                            _setViewValue(val, triggerUndo, true);
                                        }, ngModelOptions.$options.debounce || 400);
                                        if (!triggerUndo) _undoKeyupTimeout = $timeout(function () {
                                            ngModel.$undoManager.push(val);
                                        }, 250);
                                        _lastKey = event.keyCode;
                                    }
                                });

                                element.on('blur', scope.events.blur = function () {
                                    _focussed = false;
                                    /* istanbul ignore else: if readonly don't update model */
                                    if (!_isReadonly) {
                                        _setViewValue(undefined, undefined, true);
                                    } else {
                                        _skipRender = true; // don't redo the whole thing, just check the placeholder logic
                                        ngModel.$render();
                                    }
                                });

                                // Placeholders not supported on ie 8 and below
                                if (attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)) {
                                    var rule;
                                    if (attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
                                    else throw('textAngular Error: An unique ID is required for placeholders to work');

                                    scope.$on('$destroy', function () {
                                        removeCSSRule(rule);
                                    });
                                }

                                element.on('focus', scope.events.focus = function () {
                                    _focussed = true;
                                    element.removeClass('placeholder-text');
                                    _reApplyOnSelectorHandlers();
                                });

                                element.on('mouseup', scope.events.mouseup = function () {
                                    var _selection = taSelection.getSelection();
                                    if (_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
                                });

                                // prevent propagation on mousedown in editor, see #206
                                element.on('mousedown', scope.events.mousedown = function (event, eventData) {
                                    /* istanbul ignore else: this is for catching the jqLite testing*/
                                    if (eventData) angular.extend(event, eventData);
                                    event.stopPropagation();
                                });
                            }
                        }

                        var selectorClickHandler = function (event) {
                            // emit the element-select event, pass the element
                            scope.$emit('ta-element-select', this);
                            event.preventDefault();
                            return false;
                        };
                        var fileDropHandler = function (event, eventData) {
                            /* istanbul ignore else: this is for catching the jqLite testing*/
                            if (eventData) angular.extend(event, eventData);
                            // emit the drop event, pass the element, preventing should be done elsewhere
                            if (!dropFired && !_isReadonly) {
                                dropFired = true;
                                var dataTransfer;
                                if (event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
                                else dataTransfer = event.dataTransfer;
                                scope.$emit('ta-drop-event', this, event, dataTransfer);
                                $timeout(function () {
                                    dropFired = false;
                                    _setViewValue(undefined, undefined, true);
                                }, 100);
                            }
                        };

                        //used for updating when inserting wrapped elements
                        var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function () {
                            /* istanbul ignore else */
                            if (!_isReadonly) angular.forEach(taSelectableElements, function (selector) {
                                // check we don't apply the handler twice
                                element.find(selector)
                                    .off('click', selectorClickHandler)
                                    .on('click', selectorClickHandler);
                            });
                        };

                        var _setInnerHTML = function (newval) {
                            element[0].innerHTML = newval;
                        };
                        var _renderTimeout;
                        var _renderInProgress = false;
                        // changes to the model variable from outside the html/text inputs
                        ngModel.$render = function () {
                            /* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
                            if (_renderInProgress) return;
                            else _renderInProgress = true;
                            // catch model being null or undefined
                            var val = ngModel.$viewValue || '';
                            // if the editor isn't focused it needs to be updated, otherwise it's receiving user input
                            if (!_skipRender) {
                                /* istanbul ignore else: in other cases we don't care */
                                if (_isContentEditable && _focussed) {
                                    // update while focussed
                                    element.removeClass('placeholder-text');
                                    if (_renderTimeout) $timeout.cancel(_renderTimeout);
                                    _renderTimeout = $timeout(function () {
                                        /* istanbul ignore if: Can't be bothered testing this... */
                                        if (!_focussed) {
                                            element[0].focus();
                                            taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
                                        }
                                        _renderTimeout = undefined;
                                    }, 1);
                                }
                                if (_isContentEditable) {
                                    // WYSIWYG Mode
                                    if (attrs.placeholder) {
                                        if (val === '') {
                                            // blank
                                            _setInnerHTML(_defaultVal);
                                        } else {
                                            // not-blank
                                            _setInnerHTML(val);
                                        }
                                    } else {
                                        _setInnerHTML((val === '') ? _defaultVal : val);
                                    }
                                    // if in WYSIWYG and readOnly we kill the use of links by clicking
                                    if (!_isReadonly) {
                                        _reApplyOnSelectorHandlers();
                                        element.on('drop', fileDropHandler);
                                    } else {
                                        element.off('drop', fileDropHandler);
                                    }
                                } else if (element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input') {
                                    // make sure the end user can SEE the html code as a display. This is a read-only display element
                                    _setInnerHTML(taApplyCustomRenderers(val));
                                } else {
                                    // only for input and textarea inputs
                                    element.val(val);
                                }
                            }
                            if (_isContentEditable && attrs.placeholder) {
                                if (val === '') {
                                    if (_focussed) element.removeClass('placeholder-text');
                                    else element.addClass('placeholder-text');
                                } else {
                                    element.removeClass('placeholder-text');
                                }
                            }
                            _renderInProgress = _skipRender = false;
                        };

                        if (attrs.taReadonly) {
                            //set initial value
                            _isReadonly = scope.$eval(attrs.taReadonly);
                            if (_isReadonly) {
                                element.addClass('ta-readonly');
                                // we changed to readOnly mode (taReadonly='true')
                                if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
                                    element.attr('disabled', 'disabled');
                                }
                                if (element.attr('contenteditable') !== undefined && element.attr('contenteditable')) {
                                    element.removeAttr('contenteditable');
                                }
                            } else {
                                element.removeClass('ta-readonly');
                                // we changed to NOT readOnly mode (taReadonly='false')
                                if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
                                    element.removeAttr('disabled');
                                } else if (_isContentEditable) {
                                    element.attr('contenteditable', 'true');
                                }
                            }
                            // taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
                            // Otherwise it is readonly by default
                            scope.$watch(attrs.taReadonly, function (newVal, oldVal) {
                                if (oldVal === newVal) return;
                                if (newVal) {
                                    element.addClass('ta-readonly');
                                    // we changed to readOnly mode (taReadonly='true')
                                    if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
                                        element.attr('disabled', 'disabled');
                                    }
                                    if (element.attr('contenteditable') !== undefined && element.attr('contenteditable')) {
                                        element.removeAttr('contenteditable');
                                    }
                                    // turn ON selector click handlers
                                    angular.forEach(taSelectableElements, function (selector) {
                                        element.find(selector).on('click', selectorClickHandler);
                                    });
                                    element.off('drop', fileDropHandler);
                                } else {
                                    element.removeClass('ta-readonly');
                                    // we changed to NOT readOnly mode (taReadonly='false')
                                    if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
                                        element.removeAttr('disabled');
                                    } else if (_isContentEditable) {
                                        element.attr('contenteditable', 'true');
                                    }
                                    // remove the selector click handlers
                                    angular.forEach(taSelectableElements, function (selector) {
                                        element.find(selector).off('click', selectorClickHandler);
                                    });
                                    element.on('drop', fileDropHandler);
                                }
                                _isReadonly = newVal;
                            });
                        }

                        // Initialise the selectableElements
                        // if in WYSIWYG and readOnly we kill the use of links by clicking
                        if (_isContentEditable && !_isReadonly) {
                            angular.forEach(taSelectableElements, function (selector) {
                                element.find(selector).on('click', selectorClickHandler);
                            });
                            element.on('drop', fileDropHandler);
                            element.on('blur', function () {
                                /* istanbul ignore next: webkit fix */
                                if (_browserDetect.webkit) { // detect webkit
                                    globalContentEditableBlur = true;
                                }
                            });
                        }
                    }
                };
            }]);

// this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
    var dropFired = false;
    var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup', 'textAngular.factories', 'textAngular.DOM', 'textAngular.validators', 'textAngular.taBind']); //This makes ngSanitize required

    textAngular.config([function () {
        // clear taTools variable. Just catches testing and any other time that this config may run multiple times...
        angular.forEach(taTools, function (value, key) {
            delete taTools[key];
        });
    }]);

    textAngular.run([function () {
        /* istanbul ignore next: not sure how to test this */
        // Require Rangy and rangy savedSelection module.
        if (typeof define === 'function' && define.amd) {
            // AMD. Register as an anonymous module.
            define(function (require) {
                window.rangy = require('rangy');
                window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
            });
        } else if (typeof require === 'function' && typeof module !== 'undefined' && typeof exports === 'object') {
            // Node/CommonJS style
            window.rangy = require('rangy');
            window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
        } else {
            // Ensure that rangy and rangy.saveSelection exists on the window (global scope).
            // TODO: Refactor so that the global scope is no longer used.
            if (!window.rangy) {
                throw("rangy-core.js and rangy-selectionsaverestore.js are required for textAngular to work correctly, rangy-core is not yet loaded.");
            } else {
                window.rangy.init();
                if (!window.rangy.saveSelection) {
                    throw("rangy-selectionsaverestore.js is required for textAngular to work correctly.");
                }
            }
        }
    }]);

    textAngular.directive("textAngular", [
        '$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand',
        'textAngularManager', '$window', '$document', '$animate', '$log', '$q', '$parse',
        function ($compile, $timeout, taOptions, taSelection, taExecCommand,
                  textAngularManager, $window, $document, $animate, $log, $q, $parse) {
            return {
                require: '?ngModel',
                scope: {},
                restrict: "EA",
                priority: 2, // So we override validators correctly
                link: function (scope, element, attrs, ngModel) {
                    // all these vars should not be accessable outside this directive
                    var _keydown, _keyup, _keypress, _mouseup, _focusin, _focusout,
                        _originalContents, _toolbars,
                        _serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
                        _taExecCommand, _resizeMouseDown, _updateSelectedStylesTimeout;

                    scope._name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial;

                    var oneEvent = function (_element, event, action) {
                        $timeout(function () {
                            // shim the .one till fixed
                            var _func = function () {
                                _element.off(event, _func);
                                action.apply(this, arguments);
                            };
                            _element.on(event, _func);
                        }, 100);
                    };
                    _taExecCommand = taExecCommand(attrs.taDefaultWrap);
                    // get the settings from the defaults and add our specific functions that need to be on the scope
                    angular.extend(scope, angular.copy(taOptions), {
                        // wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
                        wrapSelection: function (command, opt, isSelectableElementTool) {
                            if (command.toLowerCase() === "undo") {
                                scope['$undoTaBindtaTextElement' + _serial]();
                            } else if (command.toLowerCase() === "redo") {
                                scope['$redoTaBindtaTextElement' + _serial]();
                            } else {
                                // catch errors like FF erroring when you try to force an undo with nothing done
                                _taExecCommand(command, false, opt, scope.defaultTagAttributes);
                                if (isSelectableElementTool) {
                                    // re-apply the selectable tool events
                                    scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
                                }
                                // refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
                                // You still have focus on the text/html input it just doesn't show up
                                scope.displayElements.text[0].focus();
                            }
                        },
                        showHtml: scope.$eval(attrs.taShowHtml) || false
                    });
                    // setup the options from the optional attributes
                    if (attrs.taFocussedClass)            scope.classes.focussed = attrs.taFocussedClass;
                    if (attrs.taTextEditorClass)            scope.classes.textEditor = attrs.taTextEditorClass;
                    if (attrs.taHtmlEditorClass)            scope.classes.htmlEditor = attrs.taHtmlEditorClass;
                    if (attrs.taDefaultTagAttributes) {
                        try {
                            //	TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
                            angular.extend(scope.defaultTagAttributes, angular.fromJson(attrs.taDefaultTagAttributes));
                        } catch (error) {
                            $log.error(error);
                        }
                    }
                    // optional setup functions
                    if (attrs.taTextEditorSetup)            scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
                    if (attrs.taHtmlEditorSetup)            scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
                    // optional fileDropHandler function
                    if (attrs.taFileDrop)                scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
                    else                                scope.fileDropHandler = scope.defaultFileDropHandler;

                    _originalContents = element[0].innerHTML;
                    // clear the original content
                    element[0].innerHTML = '';

                    // Setup the HTML elements as variable references for use later
                    scope.displayElements = {
                        // we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
                        // wheras the input will ALLWAYS have the correct value.
                        forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
                        html: angular.element("<textarea></textarea>"),
                        text: angular.element("<div></div>"),
                        // other toolbased elements
                        scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
                        popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
                        popoverArrow: angular.element('<div class="arrow"></div>'),
                        popoverContainer: angular.element('<div class="popover-content"></div>'),
                        resize: {
                            overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
                            background: angular.element('<div class="ta-resizer-handle-background"></div>'),
                            anchors: [
                                angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
                                angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
                                angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
                                angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
                            ],
                            info: angular.element('<div class="ta-resizer-handle-info"></div>')
                        }
                    };

                    // Setup the popover
                    scope.displayElements.popover.append(scope.displayElements.popoverArrow);
                    scope.displayElements.popover.append(scope.displayElements.popoverContainer);
                    scope.displayElements.scrollWindow.append(scope.displayElements.popover);

                    scope.displayElements.popover.on('mousedown', function (e, eventData) {
                        /* istanbul ignore else: this is for catching the jqLite testing*/
                        if (eventData) angular.extend(e, eventData);
                        // this prevents focusout from firing on the editor when clicking anything in the popover
                        e.preventDefault();
                        return false;
                    });

                    // define the popover show and hide functions
                    scope.showPopover = function (_el) {
                        scope.displayElements.popover.css('display', 'block');
                        scope.reflowPopover(_el);
                        $animate.addClass(scope.displayElements.popover, 'in');
                        oneEvent($document.find('body'), 'click keyup', function () {
                            scope.hidePopover();
                        });
                    };
                    scope.reflowPopover = function (_el) {
                        /* istanbul ignore if: catches only if near bottom of editor */
                        if (scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop) {
                            scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + scope.displayElements.scrollWindow[0].scrollTop + 'px');
                            scope.displayElements.popover.removeClass('top').addClass('bottom');
                        } else {
                            scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + scope.displayElements.scrollWindow[0].scrollTop + 'px');
                            scope.displayElements.popover.removeClass('bottom').addClass('top');
                        }
                        var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
                        var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
                        scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
                        scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
                    };
                    scope.hidePopover = function () {
                        scope.displayElements.popover.css('display', '');
                        scope.displayElements.popoverContainer.attr('style', '');
                        scope.displayElements.popoverContainer.attr('class', 'popover-content');
                        scope.displayElements.popover.removeClass('in');
                    };

                    // setup the resize overlay
                    scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
                    angular.forEach(scope.displayElements.resize.anchors, function (anchor) {
                        scope.displayElements.resize.overlay.append(anchor);
                    });
                    scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
                    scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);

                    // define the show and hide events
                    scope.reflowResizeOverlay = function (_el) {
                        _el = angular.element(_el)[0];
                        scope.displayElements.resize.overlay.css({
                            'display': 'block',
                            'left': _el.offsetLeft - 5 + 'px',
                            'top': _el.offsetTop - 5 + 'px',
                            'width': _el.offsetWidth + 10 + 'px',
                            'height': _el.offsetHeight + 10 + 'px'
                        });
                        scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
                    };
                    /* istanbul ignore next: pretty sure phantomjs won't test this */
                    scope.showResizeOverlay = function (_el) {
                        var _body = $document.find('body');
                        _resizeMouseDown = function (event) {
                            var startPosition = {
                                width: parseInt(_el.attr('width')),
                                height: parseInt(_el.attr('height')),
                                x: event.clientX,
                                y: event.clientY
                            };
                            if (startPosition.width === undefined || isNaN(startPosition.width)) startPosition.width = _el[0].offsetWidth;
                            if (startPosition.height === undefined || isNaN(startPosition.height)) startPosition.height = _el[0].offsetHeight;
                            scope.hidePopover();
                            var ratio = startPosition.height / startPosition.width;
                            var mousemove = function (event) {
                                // calculate new size
                                var pos = {
                                    x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
                                    y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
                                };

                                // DEFAULT: the aspect ratio is not locked unless the Shift key is pressed.
                                //
                                // attribute: ta-resize-force-aspect-ratio -- locks resize into maintaing the aspect ratio
                                var bForceAspectRatio = (attrs.taResizeForceAspectRatio !== undefined);
                                // attribute: ta-resize-maintain-aspect-ratio=true causes the space ratio to remain locked
                                // unless the Shift key is pressed
                                var bFlipKeyBinding = attrs.taResizeMaintainAspectRatio;
                                var bKeepRatio = bForceAspectRatio || (bFlipKeyBinding && !event.shiftKey);
                                if (bKeepRatio) {
                                    var newRatio = pos.y / pos.x;
                                    pos.x = ratio > newRatio ? pos.x : pos.y / ratio;
                                    pos.y = ratio > newRatio ? pos.x * ratio : pos.y;
                                }
                                var el = angular.element(_el);

                                function roundedMaxVal(val) {
                                    return Math.round(Math.max(0, val));
                                }

                                el.css('height', roundedMaxVal(pos.y) + 'px');
                                el.css('width', roundedMaxVal(pos.x) + 'px');

                                // reflow the popover tooltip
                                scope.reflowResizeOverlay(_el);
                            };
                            _body.on('mousemove', mousemove);
                            oneEvent(_body, 'mouseup', function (event) {
                                event.preventDefault();
                                event.stopPropagation();
                                _body.off('mousemove', mousemove);
                                // at this point, we need to force the model to update! since the css has changed!
                                // this fixes bug: #862 - we now hide the popover -- as this seems more consitent.
                                // there are still issues under firefox, the window does not repaint. -- not sure
                                // how best to resolve this, but clicking anywhere works.
                                scope.$apply(function () {
                                    scope.hidePopover();
                                    scope.updateTaBindtaTextElement();
                                }, 100);
                            });
                            event.stopPropagation();
                            event.preventDefault();
                        };

                        scope.displayElements.resize.anchors[3].off('mousedown');
                        scope.displayElements.resize.anchors[3].on('mousedown', _resizeMouseDown);

                        scope.reflowResizeOverlay(_el);
                        oneEvent(_body, 'click', function () {
                            scope.hideResizeOverlay();
                        });
                    };
                    /* istanbul ignore next: pretty sure phantomjs won't test this */
                    scope.hideResizeOverlay = function () {
                        scope.displayElements.resize.anchors[3].off('mousedown', _resizeMouseDown);
                        scope.displayElements.resize.overlay.css('display', '');
                    };

                    // allow for insertion of custom directives on the textarea and div
                    scope.setup.htmlEditorSetup(scope.displayElements.html);
                    scope.setup.textEditorSetup(scope.displayElements.text);
                    scope.displayElements.html.attr({
                        'id': 'taHtmlElement' + _serial,
                        'ng-show': 'showHtml',
                        'ta-bind': 'ta-bind',
                        'ng-model': 'html',
                        'ng-model-options': element.attr('ng-model-options')
                    });
                    scope.displayElements.text.attr({
                        'id': 'taTextElement' + _serial,
                        'contentEditable': 'true',
                        'ta-bind': 'ta-bind',
                        'ng-model': 'html',
                        'ng-model-options': element.attr('ng-model-options')
                    });
                    scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
                    if (attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);

                    if (attrs.taUnsafeSanitizer) {
                        scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
                        scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
                    }

                    // add the main elements to the origional element
                    scope.displayElements.scrollWindow.append(scope.displayElements.text);
                    element.append(scope.displayElements.scrollWindow);
                    element.append(scope.displayElements.html);

                    scope.displayElements.forminput.attr('name', scope._name);
                    element.append(scope.displayElements.forminput);

                    if (attrs.tabindex) {
                        element.removeAttr('tabindex');
                        scope.displayElements.text.attr('tabindex', attrs.tabindex);
                        scope.displayElements.html.attr('tabindex', attrs.tabindex);
                    }

                    if (attrs.placeholder) {
                        scope.displayElements.text.attr('placeholder', attrs.placeholder);
                        scope.displayElements.html.attr('placeholder', attrs.placeholder);
                    }

                    if (attrs.taDisabled) {
                        scope.displayElements.text.attr('ta-readonly', 'disabled');
                        scope.displayElements.html.attr('ta-readonly', 'disabled');
                        scope.disabled = scope.$parent.$eval(attrs.taDisabled);
                        scope.$parent.$watch(attrs.taDisabled, function (newVal) {
                            scope.disabled = newVal;
                            if (scope.disabled) {
                                element.addClass(scope.classes.disabled);
                            } else {
                                element.removeClass(scope.classes.disabled);
                            }
                        });
                    }

                    if (attrs.taPaste) {
                        scope._pasteHandler = function (_html) {
                            return $parse(attrs.taPaste)(scope.$parent, {$html: _html});
                        };
                        scope.displayElements.text.attr('ta-paste', '_pasteHandler($html)');
                    }

                    // compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
                    $compile(scope.displayElements.scrollWindow)(scope);
                    $compile(scope.displayElements.html)(scope);

                    scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
                    scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];

                    // add the classes manually last
                    element.addClass("ta-root");
                    scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
                    scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);

                    // used in the toolbar actions
                    scope._actionRunning = false;
                    var _savedSelection = false;
                    scope.startAction = function () {
                        scope._actionRunning = true;
                        // if rangy library is loaded return a function to reload the current selection
                        _savedSelection = $window.rangy.saveSelection();
                        return function () {
                            if (_savedSelection) $window.rangy.restoreSelection(_savedSelection);
                        };
                    };
                    scope.endAction = function () {
                        scope._actionRunning = false;
                        if (_savedSelection) {
                            if (scope.showHtml) {
                                scope.displayElements.html[0].focus();
                            } else {
                                scope.displayElements.text[0].focus();
                            }
                            // $window.rangy.restoreSelection(_savedSelection);
                            $window.rangy.removeMarkers(_savedSelection);
                        }
                        _savedSelection = false;
                        scope.updateSelectedStyles();
                        // only update if in text or WYSIWYG mode
                        if (!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
                    };

                    // note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
                    // cascades to displayElements.text and displayElements.html automatically.
                    _focusin = function () {
                        scope.focussed = true;
                        element.addClass(scope.classes.focussed);
                        _toolbars.focus();
                        element.triggerHandler('focus');
                    };
                    scope.displayElements.html.on('focus', _focusin);
                    scope.displayElements.text.on('focus', _focusin);
                    _focusout = function (e) {
                        // if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
                        if (!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]) {
                            element.removeClass(scope.classes.focussed);
                            _toolbars.unfocus();
                            // to prevent multiple apply error defer to next seems to work.
                            $timeout(function () {
                                scope._bUpdateSelectedStyles = false;
                                element.triggerHandler('blur');
                                scope.focussed = false;
                            }, 0);
                        }
                        e.preventDefault();
                        return false;
                    };
                    scope.displayElements.html.on('blur', _focusout);
                    scope.displayElements.text.on('blur', _focusout);

                    scope.displayElements.text.on('paste', function (event) {
                        element.triggerHandler('paste', event);
                    });

                    // Setup the default toolbar tools, this way allows the user to add new tools like plugins.
                    // This is on the editor for future proofing if we find a better way to do this.
                    scope.queryFormatBlockState = function (command) {
                        // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
                        return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
                    };
                    scope.queryCommandState = function (command) {
                        // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
                        return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
                    };
                    scope.switchView = function () {
                        scope.showHtml = !scope.showHtml;
                        $animate.enabled(false, scope.displayElements.html);
                        $animate.enabled(false, scope.displayElements.text);
                        //Show the HTML view
                        if (scope.showHtml) {
                            //defer until the element is visible
                            $timeout(function () {
                                $animate.enabled(true, scope.displayElements.html);
                                $animate.enabled(true, scope.displayElements.text);
                                // [0] dereferences the DOM object from the angular.element
                                return scope.displayElements.html[0].focus();
                            }, 100);
                        } else {
                            //Show the WYSIWYG view
                            //defer until the element is visible
                            $timeout(function () {
                                $animate.enabled(true, scope.displayElements.html);
                                $animate.enabled(true, scope.displayElements.text);
                                // [0] dereferences the DOM object from the angular.element
                                return scope.displayElements.text[0].focus();
                            }, 100);
                        }
                    };

                    // changes to the model variable from outside the html/text inputs
                    // if no ngModel, then the only input is from inside text-angular
                    if (attrs.ngModel) {
                        var _firstRun = true;
                        ngModel.$render = function () {
                            if (_firstRun) {
                                // we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
                                _firstRun = false;
                                // if view value is null or undefined initially and there was original content, set to the original content
                                var _initialValue = scope.$parent.$eval(attrs.ngModel);
                                if ((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')) {
                                    // on passing through to taBind it will be sanitised
                                    ngModel.$setViewValue(_originalContents);
                                }
                            }
                            scope.displayElements.forminput.val(ngModel.$viewValue);
                            // if the editors aren't focused they need to be updated, otherwise they are doing the updating
                            scope.html = ngModel.$viewValue || '';
                        };
                        // trigger the validation calls
                        if (element.attr('required')) ngModel.$validators.required = function (modelValue, viewValue) {
                            var value = modelValue || viewValue;
                            return !(!value || value.trim() === '');
                        };
                    } else {
                        // if no ngModel then update from the contents of the origional html.
                        scope.displayElements.forminput.val(_originalContents);
                        scope.html = _originalContents;
                    }

                    // changes from taBind back up to here
                    scope.$watch('html', function (newValue, oldValue) {
                        if (newValue !== oldValue) {
                            if (attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
                            scope.displayElements.forminput.val(newValue);
                        }
                    });

                    if (attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(scope._name, scope, attrs.taTargetToolbars.split(','));
                    else {
                        var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
                        // passthrough init of toolbar options
                        if (attrs.taToolbar)                        _toolbar.attr('ta-toolbar', attrs.taToolbar);
                        if (attrs.taToolbarClass)                _toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
                        if (attrs.taToolbarGroupClass)            _toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
                        if (attrs.taToolbarButtonClass)            _toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
                        if (attrs.taToolbarActiveButtonClass)    _toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
                        if (attrs.taFocussedClass)                _toolbar.attr('ta-focussed-class', attrs.taFocussedClass);

                        element.prepend(_toolbar);
                        $compile(_toolbar)(scope.$parent);
                        _toolbars = textAngularManager.registerEditor(scope._name, scope, ['textAngularToolbar' + _serial]);
                    }

                    scope.$on('$destroy', function () {
                        textAngularManager.unregisterEditor(scope._name);
                        angular.element(window).off('blur');
                    });

                    // catch element select event and pass to toolbar tools
                    scope.$on('ta-element-select', function (event, element) {
                        if (_toolbars.triggerElementSelect(event, element)) {
                            scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
                        }
                    });

                    scope.$on('ta-drop-event', function (event, element, dropEvent, dataTransfer) {
                        scope.displayElements.text[0].focus();
                        if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
                            angular.forEach(dataTransfer.files, function (file) {
                                // taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
                                // If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
                                // Once one of these has been executed wrap the result as a promise, if undefined or variable update the taBind, else we should wait for the promise
                                try {
                                    $q.when(scope.fileDropHandler(file, scope.wrapSelection) ||
                                        (scope.fileDropHandler !== scope.defaultFileDropHandler &&
                                        $q.when(scope.defaultFileDropHandler(file, scope.wrapSelection)))).then(function () {
                                        scope['updateTaBindtaTextElement' + _serial]();
                                    });
                                } catch (error) {
                                    $log.error(error);
                                }
                            });
                            dropEvent.preventDefault();
                            dropEvent.stopPropagation();
                            /* istanbul ignore else, the updates if moved text */
                        } else {
                            $timeout(function () {
                                scope['updateTaBindtaTextElement' + _serial]();
                            }, 0);
                        }
                    });

                    // the following is for applying the active states to the tools that support it
                    scope._bUpdateSelectedStyles = false;
                    /* istanbul ignore next: browser window/tab leave check */
                    angular.element(window).on('blur', function () {
                        scope._bUpdateSelectedStyles = false;
                        scope.focussed = false;
                    });
                    // loop through all the tools polling their activeState function if it exists
                    scope.updateSelectedStyles = function () {
                        var _selection;
                        /* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
                        if (_updateSelectedStylesTimeout) $timeout.cancel(_updateSelectedStylesTimeout);
                        // test if the common element ISN'T the root ta-text node
                        if ((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]) {
                            _toolbars.updateSelectedStyles(angular.element(_selection));
                        } else _toolbars.updateSelectedStyles();
                        // used to update the active state when a key is held down, ie the left arrow
                        /* istanbul ignore else: browser only check */
                        if (scope._bUpdateSelectedStyles) _updateSelectedStylesTimeout = $timeout(scope.updateSelectedStyles, 200);
                    };
                    // start updating on keydown
                    _keydown = function () {
                        /* istanbul ignore next: ie catch */
                        if (!scope.focussed) {
                            scope._bUpdateSelectedStyles = false;
                            return;
                        }
                        /* istanbul ignore else: don't run if already running */
                        if (!scope._bUpdateSelectedStyles) {
                            scope._bUpdateSelectedStyles = true;
                            scope.$apply(function () {
                                scope.updateSelectedStyles();
                            });
                        }
                    };
                    scope.displayElements.html.on('keydown', _keydown);
                    scope.displayElements.text.on('keydown', _keydown);
                    // stop updating on key up and update the display/model
                    _keyup = function () {
                        scope._bUpdateSelectedStyles = false;
                    };
                    scope.displayElements.html.on('keyup', _keyup);
                    scope.displayElements.text.on('keyup', _keyup);
                    // stop updating on key up and update the display/model
                    _keypress = function (event, eventData) {
                        /* istanbul ignore else: this is for catching the jqLite testing*/
                        if (eventData) angular.extend(event, eventData);
                        scope.$apply(function () {
                            if (_toolbars.sendKeyCommand(event)) {
                                /* istanbul ignore else: don't run if already running */
                                if (!scope._bUpdateSelectedStyles) {
                                    scope.updateSelectedStyles();
                                }
                                event.preventDefault();
                                return false;
                            }
                        });
                    };
                    scope.displayElements.html.on('keypress', _keypress);
                    scope.displayElements.text.on('keypress', _keypress);
                    // update the toolbar active states when we click somewhere in the text/html boxed
                    _mouseup = function () {
                        // ensure only one execution of updateSelectedStyles()
                        scope._bUpdateSelectedStyles = false;
                        scope.$apply(function () {
                            scope.updateSelectedStyles();
                        });
                    };
                    scope.displayElements.html.on('mouseup', _mouseup);
                    scope.displayElements.text.on('mouseup', _mouseup);
                }
            };
        }
    ]);
    textAngular.service('textAngularManager', ['taToolExecuteAction', 'taTools', 'taRegisterTool', function (taToolExecuteAction, taTools, taRegisterTool) {
        // this service is used to manage all textAngular editors and toolbars.
        // All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
        // these contain references to all the editors and toolbars that have been initialised in this app
        var toolbars = {}, editors = {};
        // when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
        // We also need to set the tools to be updated to be the toolbars...
        return {
            // register an editor and the toolbars that it is affected by
            registerEditor: function (name, scope, targetToolbars) {
                // targetToolbars are optional, we don't require a toolbar to function
                if (!name || name === '') throw('textAngular Error: An editor requires a name');
                if (!scope) throw('textAngular Error: An editor requires a scope');
                if (editors[name]) throw('textAngular Error: An Editor with name "' + name + '" already exists');
                // _toolbars is an ARRAY of toolbar scopes
                var _toolbars = [];
                angular.forEach(targetToolbars, function (_name) {
                    if (toolbars[_name]) _toolbars.push(toolbars[_name]);
                    // if it doesn't exist it may not have been compiled yet and it will be added later
                });
                editors[name] = {
                    scope: scope,
                    toolbars: targetToolbars,
                    _registerToolbar: function (toolbarScope) {
                        // add to the list late
                        if (this.toolbars.indexOf(toolbarScope.name) >= 0) _toolbars.push(toolbarScope);
                    },
                    // this is a suite of functions the editor should use to update all it's linked toolbars
                    editorFunctions: {
                        disable: function () {
                            // disable all linked toolbars
                            angular.forEach(_toolbars, function (toolbarScope) {
                                toolbarScope.disabled = true;
                            });
                        },
                        enable: function () {
                            // enable all linked toolbars
                            angular.forEach(_toolbars, function (toolbarScope) {
                                toolbarScope.disabled = false;
                            });
                        },
                        focus: function () {
                            // this should be called when the editor is focussed
                            angular.forEach(_toolbars, function (toolbarScope) {
                                toolbarScope._parent = scope;
                                toolbarScope.disabled = false;
                                toolbarScope.focussed = true;
                                scope.focussed = true;
                            });
                        },
                        unfocus: function () {
                            // this should be called when the editor becomes unfocussed
                            angular.forEach(_toolbars, function (toolbarScope) {
                                toolbarScope.disabled = true;
                                toolbarScope.focussed = false;
                            });
                            scope.focussed = false;
                        },
                        updateSelectedStyles: function (selectedElement) {
                            // update the active state of all buttons on liked toolbars
                            angular.forEach(_toolbars, function (toolbarScope) {
                                angular.forEach(toolbarScope.tools, function (toolScope) {
                                    if (toolScope.activeState) {
                                        toolbarScope._parent = scope;
                                        toolScope.active = toolScope.activeState(selectedElement);
                                    }
                                });
                            });
                        },
                        sendKeyCommand: function (event) {
                            // we return true if we applied an action, false otherwise
                            var result = false;
                            if (event.ctrlKey || event.metaKey || event.specialKey) angular.forEach(taTools, function (tool, name) {
                                if (tool.commandKeyCode && (tool.commandKeyCode === event.which || tool.commandKeyCode === event.specialKey)) {
                                    for (var _t = 0; _t < _toolbars.length; _t++) {
                                        if (_toolbars[_t].tools[name] !== undefined) {
                                            taToolExecuteAction.call(_toolbars[_t].tools[name], scope);
                                            result = true;
                                            break;
                                        }
                                    }
                                }
                            });
                            return result;
                        },
                        triggerElementSelect: function (event, element) {
                            // search through the taTools to see if a match for the tag is made.
                            // if there is, see if the tool is on a registered toolbar and not disabled.
                            // NOTE: This can trigger on MULTIPLE tools simultaneously.
                            var elementHasAttrs = function (_element, attrs) {
                                var result = true;
                                for (var i = 0; i < attrs.length; i++) result = result && _element.attr(attrs[i]);
                                return result;
                            };
                            var workerTools = [];
                            var unfilteredTools = {};
                            var result = false;
                            element = angular.element(element);
                            // get all valid tools by element name, keep track if one matches the
                            var onlyWithAttrsFilter = false;
                            angular.forEach(taTools, function (tool, name) {
                                if (
                                    tool.onElementSelect &&
                                    tool.onElementSelect.element &&
                                    tool.onElementSelect.element.toLowerCase() === element[0].tagName.toLowerCase() &&
                                    (!tool.onElementSelect.filter || tool.onElementSelect.filter(element))
                                ) {
                                    // this should only end up true if the element matches the only attributes
                                    onlyWithAttrsFilter = onlyWithAttrsFilter ||
                                        (angular.isArray(tool.onElementSelect.onlyWithAttrs) && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs));
                                    if (!tool.onElementSelect.onlyWithAttrs || elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) unfilteredTools[name] = tool;
                                }
                            });
                            // if we matched attributes to filter on, then filter, else continue
                            if (onlyWithAttrsFilter) {
                                angular.forEach(unfilteredTools, function (tool, name) {
                                    if (tool.onElementSelect.onlyWithAttrs && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) workerTools.push({
                                        'name': name,
                                        'tool': tool
                                    });
                                });
                                // sort most specific (most attrs to find) first
                                workerTools.sort(function (a, b) {
                                    return b.tool.onElementSelect.onlyWithAttrs.length - a.tool.onElementSelect.onlyWithAttrs.length;
                                });
                            } else {
                                angular.forEach(unfilteredTools, function (tool, name) {
                                    workerTools.push({'name': name, 'tool': tool});
                                });
                            }
                            // Run the actions on the first visible filtered tool only
                            if (workerTools.length > 0) {
                                for (var _i = 0; _i < workerTools.length; _i++) {
                                    var tool = workerTools[_i].tool;
                                    var name = workerTools[_i].name;
                                    for (var _t = 0; _t < _toolbars.length; _t++) {
                                        if (_toolbars[_t].tools[name] !== undefined) {
                                            tool.onElementSelect.action.call(_toolbars[_t].tools[name], event, element, scope);
                                            result = true;
                                            break;
                                        }
                                    }
                                    if (result) break;
                                }
                            }
                            return result;
                        }
                    }
                };
                return editors[name].editorFunctions;
            },
            // retrieve editor by name, largely used by testing suites only
            retrieveEditor: function (name) {
                return editors[name];
            },
            unregisterEditor: function (name) {
                delete editors[name];
            },
            // registers a toolbar such that it can be linked to editors
            registerToolbar: function (scope) {
                if (!scope) throw('textAngular Error: A toolbar requires a scope');
                if (!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
                if (toolbars[scope.name]) throw('textAngular Error: A toolbar with name "' + scope.name + '" already exists');
                toolbars[scope.name] = scope;
                angular.forEach(editors, function (_editor) {
                    _editor._registerToolbar(scope);
                });
            },
            // retrieve toolbar by name, largely used by testing suites only
            retrieveToolbar: function (name) {
                return toolbars[name];
            },
            // retrieve toolbars by editor name, largely used by testing suites only
            retrieveToolbarsViaEditor: function (name) {
                var result = [], _this = this;
                angular.forEach(this.retrieveEditor(name).toolbars, function (name) {
                    result.push(_this.retrieveToolbar(name));
                });
                return result;
            },
            unregisterToolbar: function (name) {
                delete toolbars[name];
            },
            // functions for updating the toolbar buttons display
            updateToolsDisplay: function (newTaTools) {
                // pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
                var _this = this;
                angular.forEach(newTaTools, function (_newTool, key) {
                    _this.updateToolDisplay(key, _newTool);
                });
            },
            // this function resets all toolbars to their default tool definitions
            resetToolsDisplay: function () {
                var _this = this;
                angular.forEach(taTools, function (_newTool, key) {
                    _this.resetToolDisplay(key);
                });
            },
            // update a tool on all toolbars
            updateToolDisplay: function (toolKey, _newTool) {
                var _this = this;
                angular.forEach(toolbars, function (toolbarScope, toolbarKey) {
                    _this.updateToolbarToolDisplay(toolbarKey, toolKey, _newTool);
                });
            },
            // resets a tool to the default/starting state on all toolbars
            resetToolDisplay: function (toolKey) {
                var _this = this;
                angular.forEach(toolbars, function (toolbarScope, toolbarKey) {
                    _this.resetToolbarToolDisplay(toolbarKey, toolKey);
                });
            },
            // update a tool on a specific toolbar
            updateToolbarToolDisplay: function (toolbarKey, toolKey, _newTool) {
                if (toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, _newTool);
                else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
            },
            // reset a tool on a specific toolbar to it's default starting value
            resetToolbarToolDisplay: function (toolbarKey, toolKey) {
                if (toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, taTools[toolKey], true);
                else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
            },
            // removes a tool from all toolbars and it's definition
            removeTool: function (toolKey) {
                delete taTools[toolKey];
                angular.forEach(toolbars, function (toolbarScope) {
                    delete toolbarScope.tools[toolKey];
                    for (var i = 0; i < toolbarScope.toolbar.length; i++) {
                        var toolbarIndex;
                        for (var j = 0; j < toolbarScope.toolbar[i].length; j++) {
                            if (toolbarScope.toolbar[i][j] === toolKey) {
                                toolbarIndex = {
                                    group: i,
                                    index: j
                                };
                                break;
                            }
                            if (toolbarIndex !== undefined) break;
                        }
                        if (toolbarIndex !== undefined) {
                            toolbarScope.toolbar[toolbarIndex.group].slice(toolbarIndex.index, 1);
                            toolbarScope._$element.children().eq(toolbarIndex.group).children().eq(toolbarIndex.index).remove();
                        }
                    }
                });
            },
            // toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
            addTool: function (toolKey, toolDefinition, group, index) {
                taRegisterTool(toolKey, toolDefinition);
                angular.forEach(toolbars, function (toolbarScope) {
                    toolbarScope.addTool(toolKey, toolDefinition, group, index);
                });
            },
            // adds a Tool but only to one toolbar not all
            addToolToToolbar: function (toolKey, toolDefinition, toolbarKey, group, index) {
                taRegisterTool(toolKey, toolDefinition);
                toolbars[toolbarKey].addTool(toolKey, toolDefinition, group, index);
            },
            // this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
            // this will call a $digest if not already happening
            refreshEditor: function (name) {
                if (editors[name]) {
                    editors[name].scope.updateTaBindtaTextElement();
                    /* istanbul ignore else: phase catch */
                    if (!editors[name].scope.$$phase) editors[name].scope.$digest();
                } else throw('textAngular Error: No Editor with name "' + name + '" exists');
            },
            // this is used by taBind to send a key command in response to a special key event
            sendKeyCommand: function (scope, event) {
                var _editor = editors[scope._name];
                /* istanbul ignore else: if nothing to do, do nothing */
                if (_editor && _editor.editorFunctions.sendKeyCommand(event)) {
                    /* istanbul ignore else: don't run if already running */
                    if (!scope._bUpdateSelectedStyles) {
                        scope.updateSelectedStyles();
                    }
                    event.preventDefault();
                    return false;
                }
            }
        };
    }]);
    textAngular.directive('textAngularToolbar', [
        '$compile', 'textAngularManager', 'taOptions', 'taTools', 'taToolExecuteAction', '$window',
        function ($compile, textAngularManager, taOptions, taTools, taToolExecuteAction, $window) {
            return {
                scope: {
                    name: '@' // a name IS required
                },
                restrict: "EA",
                link: function (scope, element, attrs) {
                    if (!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
                    angular.extend(scope, angular.copy(taOptions));
                    if (attrs.taToolbar)                        scope.toolbar = scope.$parent.$eval(attrs.taToolbar);
                    if (attrs.taToolbarClass)                scope.classes.toolbar = attrs.taToolbarClass;
                    if (attrs.taToolbarGroupClass)            scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
                    if (attrs.taToolbarButtonClass)            scope.classes.toolbarButton = attrs.taToolbarButtonClass;
                    if (attrs.taToolbarActiveButtonClass)    scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
                    if (attrs.taFocussedClass)                scope.classes.focussed = attrs.taFocussedClass;

                    scope.disabled = true;
                    scope.focussed = false;
                    scope._$element = element;
                    element[0].innerHTML = '';
                    element.addClass("ta-toolbar " + scope.classes.toolbar);

                    scope.$watch('focussed', function () {
                        if (scope.focussed) element.addClass(scope.classes.focussed);
                        else element.removeClass(scope.classes.focussed);
                    });

                    var setupToolElement = function (toolDefinition, toolScope) {
                        var toolElement;
                        if (toolDefinition && toolDefinition.display) {
                            toolElement = angular.element(toolDefinition.display);
                        }
                        else toolElement = angular.element("<button type='button'>");

                        if (toolDefinition && toolDefinition["class"]) toolElement.addClass(toolDefinition["class"]);
                        else toolElement.addClass(scope.classes.toolbarButton);

                        toolElement.attr('name', toolScope.name);
                        // important to not take focus from the main text/html entry
                        toolElement.attr('ta-button', 'ta-button');
                        toolElement.attr('ng-disabled', 'isDisabled()');
                        toolElement.attr('tabindex', '-1');
                        toolElement.attr('ng-click', 'executeAction()');
                        toolElement.attr('ng-class', 'displayActiveToolClass(active)');

                        if (toolDefinition && toolDefinition.tooltiptext) {
                            toolElement.attr('title', toolDefinition.tooltiptext);
                        }
                        if (toolDefinition && !toolDefinition.display && !toolScope._display) {
                            // first clear out the current contents if any
                            toolElement[0].innerHTML = '';
                            // add the buttonText
                            if (toolDefinition.buttontext) toolElement[0].innerHTML = toolDefinition.buttontext;
                            // add the icon to the front of the button if there is content
                            if (toolDefinition.iconclass) {
                                var icon = angular.element('<i>'), content = toolElement[0].innerHTML;
                                icon.addClass(toolDefinition.iconclass);
                                toolElement[0].innerHTML = '';
                                toolElement.append(icon);
                                if (content && content !== '') toolElement.append('&nbsp;' + content);
                            }
                        }

                        toolScope._lastToolDefinition = angular.copy(toolDefinition);

                        return $compile(toolElement)(toolScope);
                    };

                    // Keep a reference for updating the active states later
                    scope.tools = {};
                    // create the tools in the toolbar
                    // default functions and values to prevent errors in testing and on init
                    scope._parent = {
                        disabled: true,
                        showHtml: false,
                        queryFormatBlockState: function () {
                            return false;
                        },
                        queryCommandState: function () {
                            return false;
                        }
                    };
                    var defaultChildScope = {
                        $window: $window,
                        $editor: function () {
                            // dynamically gets the editor as it is set
                            return scope._parent;
                        },
                        isDisabled: function () {
                            // to set your own disabled logic set a function or boolean on the tool called 'disabled'
                            return ( // this bracket is important as without it it just returns the first bracket and ignores the rest
                                // when the button's disabled function/value evaluates to true
                                (typeof this.$eval('disabled') !== 'function' && this.$eval('disabled')) || this.$eval('disabled()') ||
                                // all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
                                (this.name !== 'html' && this.$editor().showHtml) ||
                                // if the toolbar is disabled
                                this.$parent.disabled ||
                                // if the current editor is disabled
                                this.$editor().disabled
                            );
                        },
                        displayActiveToolClass: function (active) {
                            return (active) ? scope.classes.toolbarButtonActive : '';
                        },
                        executeAction: taToolExecuteAction
                    };

                    angular.forEach(scope.toolbar, function (group) {
                        // setup the toolbar group
                        var groupElement = angular.element("<div>");
                        groupElement.addClass(scope.classes.toolbarGroup);
                        angular.forEach(group, function (tool) {
                            // init and add the tools to the group
                            // a tool name (key name from taTools struct)
                            //creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
                            // reference to the scope and element kept
                            scope.tools[tool] = angular.extend(scope.$new(true), taTools[tool], defaultChildScope, {name: tool});
                            scope.tools[tool].$element = setupToolElement(taTools[tool], scope.tools[tool]);
                            // append the tool compiled with the childScope to the group element
                            groupElement.append(scope.tools[tool].$element);
                        });
                        // append the group to the toolbar
                        element.append(groupElement);
                    });

                    // update a tool
                    // if a value is set to null, remove from the display
                    // when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
                    // to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
                    scope.updateToolDisplay = function (key, _newTool, forceNew) {
                        var toolInstance = scope.tools[key];
                        if (toolInstance) {
                            // get the last toolDefinition, then override with the new definition
                            if (toolInstance._lastToolDefinition && !forceNew) _newTool = angular.extend({}, toolInstance._lastToolDefinition, _newTool);
                            if (_newTool.buttontext === null && _newTool.iconclass === null && _newTool.display === null)
                                throw('textAngular Error: Tool Definition for updating "' + key + '" does not have a valid display/iconclass/buttontext value');

                            // if tool is defined on this toolbar, update/redo the tool
                            if (_newTool.buttontext === null) {
                                delete _newTool.buttontext;
                            }
                            if (_newTool.iconclass === null) {
                                delete _newTool.iconclass;
                            }
                            if (_newTool.display === null) {
                                delete _newTool.display;
                            }

                            var toolElement = setupToolElement(_newTool, toolInstance);
                            toolInstance.$element.replaceWith(toolElement);
                            toolInstance.$element = toolElement;
                        }
                    };

                    // we assume here that all values passed are valid and correct
                    scope.addTool = function (key, _newTool, groupIndex, index) {
                        scope.tools[key] = angular.extend(scope.$new(true), taTools[key], defaultChildScope, {name: key});
                        scope.tools[key].$element = setupToolElement(taTools[key], scope.tools[key]);
                        var group;
                        if (groupIndex === undefined) groupIndex = scope.toolbar.length - 1;
                        group = angular.element(element.children()[groupIndex]);

                        if (index === undefined) {
                            group.append(scope.tools[key].$element);
                            scope.toolbar[groupIndex][scope.toolbar[groupIndex].length - 1] = key;
                        } else {
                            group.children().eq(index).after(scope.tools[key].$element);
                            scope.toolbar[groupIndex][index] = key;
                        }
                    };

                    textAngularManager.registerToolbar(scope);

                    scope.$on('$destroy', function () {
                        textAngularManager.unregisterToolbar(scope.name);
                    });
                }
            };
        }
    ]);
})();